diff --git a/.metadata b/.metadata index 3e6084b..c6834e0 100644 --- a/.metadata +++ b/.metadata @@ -18,21 +18,6 @@ migration: - platform: android create_revision: 72432c3f15b26fe55f8fd822e5fb3581260f75dd base_revision: 72432c3f15b26fe55f8fd822e5fb3581260f75dd - - platform: ios - create_revision: 72432c3f15b26fe55f8fd822e5fb3581260f75dd - base_revision: 72432c3f15b26fe55f8fd822e5fb3581260f75dd - - platform: linux - create_revision: 72432c3f15b26fe55f8fd822e5fb3581260f75dd - base_revision: 72432c3f15b26fe55f8fd822e5fb3581260f75dd - - platform: macos - create_revision: 72432c3f15b26fe55f8fd822e5fb3581260f75dd - base_revision: 72432c3f15b26fe55f8fd822e5fb3581260f75dd - - platform: web - create_revision: 72432c3f15b26fe55f8fd822e5fb3581260f75dd - base_revision: 72432c3f15b26fe55f8fd822e5fb3581260f75dd - - platform: windows - create_revision: 72432c3f15b26fe55f8fd822e5fb3581260f75dd - base_revision: 72432c3f15b26fe55f8fd822e5fb3581260f75dd # User provided section diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 024634a..d0dd89a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,7 @@ + + + post(String path, {dynamic data, Options? options}) async { + try { + final response = await _dio.post( + path, + data: data, + options: + options ?? Options(headers: {'Content-Type': 'application/json'}), + ); + return response; + } on DioError catch (e) { + print('DioError: ${e.response?.data ?? e.message}'); + rethrow; // or handle specific DioError here + } catch (e) { + print('Unexpected error: $e'); + rethrow; + } + } + + Future refreshAccessToken(String refreshToken) async { + try { + final response = await _dio.post( + '/refreshToken', + data: {'REFRESH_TOKEN': refreshToken}, + options: Options( + headers: { + 'Content-Type': 'application/json', + }, + ), + ); + return response; + } catch (e) { + print('Refresh Token error: $e'); + rethrow; + } + } + + Future updateUserProfile( + String id, FormData formData, String token) async { + try { + final response = await _dio.put( + '/user/update/$id', + data: formData, + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'multipart/form-data', + }, + ), + ); + return response; + } catch (e) { + print('Update Profile error: $e'); + rethrow; + } + } + + Future updatePassword( + String id, + Map data, + String token, + ) async { + try { + final response = await _dio.put( + '/user/update/password/$id', + data: data, + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ), + ); + return response; + } catch (e) { + print('Update Password error: $e'); + rethrow; + } + } + + Future reportIssue( + Map data, + String token, + ) async { + try { + final response = await _dio.post('/report', + data: data, + options: Options(headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + })); + return response; + } catch (e) { + print('Update Password error: $e'); + rethrow; + } + } + + Future registerStudent(Map data) async { + try { + // Send POST request to the registration endpoint + final response = await _dio.post( + '/register/student', + data: data, + options: Options( + headers: { + 'Content-Type': 'application/json', + }, + ), + ); + return response; + } catch (e) { + // Handle the error + rethrow; + } + } + + Future loginStudent(Map data) async { + try { + final response = await _dio.post( + '/login', + data: data, + options: Options( + headers: { + 'Content-Type': 'application/json', + }, + ), + ); + return response; + } catch (e) { + rethrow; + } + } + + Future forgotPassword(Map data) async { + try { + final response = await _dio.post( + '/forgotPassword', + data: data, + options: Options( + headers: { + 'Content-Type': 'application/json', + }, + ), + ); + return response; + } catch (e) { + rethrow; + } + } + + Future getMe(String token) async { + try { + print('Sending getMe request with token: Bearer $token'); + final response = await _dio.get( + '/getMe', + options: Options( + headers: { + 'Authorization': 'Bearer $token', // Add 'Bearer ' prefix here + }, + ), + ); + print('getMe response: ${response.data}'); + return response; + } catch (e) { + print('GetMe error: $e'); + rethrow; + } + } + + Future getSections(String token) async { + try { + final response = await _dio.get( + '/section', + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ), + ); + print('getSections response: ${response.data}'); + return response; + } catch (e) { + print('GetSections error: $e'); + rethrow; + } + } + + Future getTopics(String sectionId, String token) async { + try { + final response = await _dio.get( + '/topic/section/$sectionId', + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ), + ); + print('getTopics response: ${response.data}'); + return response; + } catch (e) { + print('GetTopics error: $e'); + rethrow; + } + } + + Future getLevels(String topicsId, String token) async { + try { + final response = await _dio.get( + '/level/topic/$topicsId', + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ), + ); + print('getLevels response: ${response.data}'); + return response; + } catch (e) { + print('GetLevels error: $e'); + rethrow; + } + } + + Future createStudentLearning(String idLevel, String token) async { + try { + final response = await _dio.post( + '/stdLearning', + data: {'ID_LEVEL': idLevel}, + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ), + ); + return response; + } catch (e) { + print('Create Student Learning error: $e'); + rethrow; + } + } + + Future getExercises(String levelId, String token) async { + try { + final response = await _dio.get( + '/exercise/level/$levelId', + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ), + ); + print('getExercises response: ${response.data}'); + return response; + } on DioError catch (e) { + print( + 'DioError: ${e.response?.statusCode} - ${e.response?.data ?? e.message}'); + rethrow; + } catch (e) { + print('Unexpected error: $e'); + rethrow; + } + } + + Future submitExerciseAnswers( + List> answers, String token) async { + try { + final response = await _dio.post( + '/stdExercise', + data: {'answers': answers}, + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ), + ); + print('submitExerciseAnswers response: ${response.data}'); + return response; + } catch (e) { + print('SubmitExerciseAnswers error: $e'); + rethrow; + } + } + + Future getScore(String stdLearningId, String token) async { + try { + final response = await _dio.get( + '/stdLearning/score/$stdLearningId', + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ), + ); + print('getScore response: ${response.data}'); + return response; + } catch (e) { + print('GetScore error: $e'); + rethrow; + } + } + + Future getLearningHistory(String sectionId, String token) async { + try { + final response = await _dio.get( + '/learningHistory/section/$sectionId', + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ), + ); + print('getLearningHistory response: ${response.data}'); + return response; + } catch (e) { + print('GetLearningHistory error: $e'); + rethrow; + } + } +} diff --git a/lib/core/services/repositories/constants.dart b/lib/core/services/repositories/constants.dart new file mode 100644 index 0000000..a568a30 --- /dev/null +++ b/lib/core/services/repositories/constants.dart @@ -0,0 +1 @@ +const String baseUrl = 'https://3311-114-6-25-184.ngrok-free.app/'; diff --git a/lib/core/services/repositories/exercise_repository.dart b/lib/core/services/repositories/exercise_repository.dart new file mode 100644 index 0000000..f4f8dc4 --- /dev/null +++ b/lib/core/services/repositories/exercise_repository.dart @@ -0,0 +1,41 @@ +import 'package:english_learning/core/services/dio_client.dart'; + +class ExerciseRepository { + final DioClient _dioClient; + + ExerciseRepository(this._dioClient); + + Future> getExercises( + String levelId, String token) async { + try { + final response = await _dioClient.getExercises(levelId, token); + if (response.statusCode == 200) { + return response.data['payload']; + } else { + throw Exception('Failed to load exercises'); + } + } catch (e) { + throw Exception('Error fetching exercises: $e'); + } + } + + Future> submitAnswersAndGetScore( + List> answers, + String studentLearningId, + String token) async { + try { + // Submit answers + await _dioClient.submitExerciseAnswers(answers, token); + + // Get score + final response = await _dioClient.getScore(studentLearningId, token); + if (response.statusCode == 200) { + return response.data['payload']; + } else { + throw Exception('Failed to get score'); + } + } catch (e) { + throw Exception('Error submitting answers and getting score: $e'); + } + } +} diff --git a/lib/core/services/repositories/history_repository.dart b/lib/core/services/repositories/history_repository.dart new file mode 100644 index 0000000..8df6b70 --- /dev/null +++ b/lib/core/services/repositories/history_repository.dart @@ -0,0 +1,37 @@ +import 'package:dio/dio.dart'; +import 'package:english_learning/core/services/dio_client.dart'; +import 'package:english_learning/features/history/models/history_model.dart'; + +class HistoryRepository { + final DioClient _dioClient; + + HistoryRepository(this._dioClient); + + Future> getLearningHistory( + String sectionId, String token) async { + try { + final response = await _dioClient.getLearningHistory(sectionId, token); + if (response.statusCode == 200 && response.data != null) { + if (response.data['payload'] != null) { + final List historyData = response.data['payload']; + return historyData + .map((json) => LearningHistory.fromJson(json)) + .toList(); + } else { + throw Exception('No history data available'); + } + } else { + throw Exception( + 'Failed to load learning history: ${response.statusMessage}'); + } + } on DioException catch (e) { + if (e.response != null) { + throw Exception('Server error: ${e.response?.statusMessage}'); + } else { + throw Exception('Network error: ${e.message}'); + } + } catch (e) { + throw Exception('Unexpected error: $e'); + } + } +} diff --git a/lib/core/services/repositories/level_repository.dart b/lib/core/services/repositories/level_repository.dart new file mode 100644 index 0000000..2660057 --- /dev/null +++ b/lib/core/services/repositories/level_repository.dart @@ -0,0 +1,35 @@ +import 'package:english_learning/core/services/dio_client.dart'; +import 'package:english_learning/features/learning/modules/level/models/level_model.dart'; + +class LevelRepository { + final DioClient _dioClient = DioClient(); + + Future> getLevels(String topicId, String token) async { + try { + final response = await _dioClient.getLevels(topicId, token); + if (response.statusCode == 200 && response.data != null) { + final Map responseData = response.data; + if (responseData.containsKey('data')) { + final Map data = responseData['data']; + final List levelsData = data['levels'] ?? []; + final List levels = + levelsData.map((json) => Level.fromJson(json)).toList(); + final Map? lastCompletedLevel = + data['lastCompletedLevel']; + + return { + 'levels': levels, + 'lastCompletedLevel': lastCompletedLevel, + }; + } else { + throw Exception('Invalid response structure: missing "data" key'); + } + } else { + throw Exception('Failed to load levels: ${response.statusCode}'); + } + } catch (e) { + print('Error in LevelRepository: $e'); + rethrow; + } + } +} diff --git a/lib/core/services/repositories/section_repository.dart b/lib/core/services/repositories/section_repository.dart new file mode 100644 index 0000000..7d1453a --- /dev/null +++ b/lib/core/services/repositories/section_repository.dart @@ -0,0 +1,22 @@ +import 'package:english_learning/core/services/dio_client.dart'; +import 'package:english_learning/features/learning/modules/model/section_model.dart'; + +class SectionRepository { + final DioClient _dioClient = DioClient(); + + Future> getSections(String token) async { + try { + final response = await _dioClient.getSections(token); + if (response.statusCode == 200) { + final Map responseData = response.data; + final List data = responseData['payload']; + return data.map((json) => Section.fromJson(json)).toList(); + } else { + throw Exception('Failed to load sections: ${response.statusCode}'); + } + } catch (e) { + print('Error in SectionRepository: $e'); + rethrow; + } + } +} diff --git a/lib/core/services/repositories/student_learning_repository.dart b/lib/core/services/repositories/student_learning_repository.dart new file mode 100644 index 0000000..7695c05 --- /dev/null +++ b/lib/core/services/repositories/student_learning_repository.dart @@ -0,0 +1,17 @@ +import 'package:english_learning/core/services/dio_client.dart'; + +class StudentLearningRepository { + final DioClient _dioClient; + + StudentLearningRepository(this._dioClient); + + Future> createStudentLearning( + String idLevel, String token) async { + try { + final response = await _dioClient.createStudentLearning(idLevel, token); + return response.data; + } catch (e) { + throw Exception('Failed to create student learning: $e'); + } + } +} diff --git a/lib/core/services/repositories/topic_repository.dart b/lib/core/services/repositories/topic_repository.dart new file mode 100644 index 0000000..8446b79 --- /dev/null +++ b/lib/core/services/repositories/topic_repository.dart @@ -0,0 +1,22 @@ +import 'package:english_learning/core/services/dio_client.dart'; +import 'package:english_learning/features/learning/modules/topics/models/topic_model.dart'; + +class TopicRepository { + final DioClient _dioClient = DioClient(); + + Future> getTopics(String sectionId, String token) async { + try { + final response = await _dioClient.getTopics(sectionId, token); + if (response.statusCode == 200) { + final Map responseData = response.data; + final List data = responseData['payload']; + return data.map((json) => Topic.fromJson(json)).toList(); + } else { + throw Exception('Failed to load topics: ${response.statusCode}'); + } + } catch (e) { + print('Error in TopicRepository: $e'); + rethrow; + } + } +} diff --git a/lib/core/services/repositories/user_repository.dart b/lib/core/services/repositories/user_repository.dart new file mode 100644 index 0000000..d391222 --- /dev/null +++ b/lib/core/services/repositories/user_repository.dart @@ -0,0 +1,119 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:english_learning/core/services/dio_client.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class UserRepository { + final DioClient dioClient = DioClient(); + final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); + + // Authentication methods + Future registerUser(Map data) async { + return await dioClient.registerStudent(data); + } + + Future loginUser(Map data) async { + return await dioClient.loginStudent(data); + } + + Future logoutUser() async { + return await dioClient.post('/logout'); + } + + Future forgotPassword(String email) async { + return await dioClient.forgotPassword({'EMAIL': email}); + } + + // User data methods + Future getMe(String token) async { + return await dioClient.getMe(token); + } + + Future updateUserProfile( + String id, + Map data, + String token, { + File? imageFile, + }) async { + try { + FormData formData = FormData.fromMap(data); + if (imageFile != null) { + formData.files.add(MapEntry( + 'PICTURE', + await MultipartFile.fromFile(imageFile.path, + filename: imageFile.path.split('/').last), + )); + } + return await dioClient.updateUserProfile(id, formData, token); + } catch (e) { + print('Update Profile error: $e'); + rethrow; + } + } + + Future updatePassword( + String id, + String oldPassword, + String newPassword, + String confirmPassword, + String token, + ) async { + try { + final data = { + "OLD_PASSWORD": oldPassword, + "PASSWORD": newPassword, + "CONFIRM_PASSWORD": confirmPassword + }; + return await dioClient.updatePassword(id, data, token); + } catch (e) { + print('Update Password error: $e'); + rethrow; + } + } + + Future reportIssue(String report, String token) async { + return await dioClient.reportIssue({'REPORTS': report}, token); + } + + // Token management methods + Future saveRefreshToken(String refreshToken) async { + await _secureStorage.write(key: 'refreshToken', value: refreshToken); + } + + Future getRefreshToken() async { + return await _secureStorage.read(key: 'refreshToken'); + } + + Future deleteRefreshToken() async { + await _secureStorage.delete(key: 'refreshToken'); + } + + Future saveToken(String token) async { + await _secureStorage.write(key: 'jwtToken', value: token); + } + + Future getToken() async { + return await _secureStorage.read(key: 'jwtToken'); + } + + Future deleteToken() async { + await _secureStorage.delete(key: 'jwtToken'); + } + + // User data storage methods + Future saveUserData(Map userData) async { + await _secureStorage.write(key: 'userData', value: json.encode(userData)); + } + + Future?> getUserData() async { + String? cachedUserData = await _secureStorage.read(key: 'userData'); + return cachedUserData != null + ? Map.from(json.decode(cachedUserData)) + : null; + } + + Future deleteUserData() async { + await _secureStorage.delete(key: 'userData'); + } +} diff --git a/lib/core/utils/styles/theme.dart b/lib/core/utils/styles/theme.dart new file mode 100644 index 0000000..3fe0619 --- /dev/null +++ b/lib/core/utils/styles/theme.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AppColors { + static const Color whiteColor = Color(0xFFFFFFFF); + static const Color blackColor = Color(0xFF262626); + static const Color blackButtonColor = Color(0xFF404040); + static const Color darkColor = Color(0xFF526071); + static const Color greyColor = Color(0xFF737373); + static const Color cardDisabledColor = Color(0xFFF2F2F2); + static const Color cardButtonColor = Color(0xFFE0E0E0); + static const Color yellowColor = Color(0xFFFDE047); + static const Color tetriaryColor = Color(0xFF959EA9); + static const Color blueColor = Color(0xFF0090FF); + static const Color redColor = Color(0xFFE9342C); + static const Color disableColor = Color(0xFFBDBDBD); + static const Color bgSoftColor = Color(0xFFF1F5FC); + static const Color sliderInActive = Color(0xFFBFDBFE); + static const Color secondaryColor = Color(0xFF5674ED); + static const Color primaryColor = Color(0xFF34C3F9); + static const Color yellowButtonColor = Color(0xFFFACC15); + + static LinearGradient get gradientTheme => const LinearGradient( + colors: [secondaryColor, primaryColor], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ); +} + +class AppTextStyles { + static TextStyle blackTextStyle = GoogleFonts.inter( + color: AppColors.blackColor, + ); + + static TextStyle logoTextStyle = GoogleFonts.comfortaa( + color: AppColors.whiteColor, + fontSize: 30, + fontWeight: FontWeight.bold, + ); + + static TextStyle greyTextStyle = GoogleFonts.inter( + color: AppColors.greyColor, + ); + + static TextStyle yellowTextStyle = GoogleFonts.inter( + color: AppColors.yellowColor, + ); + + static TextStyle redTextStyle = GoogleFonts.inter( + color: AppColors.redColor, + ); + + static TextStyle disableTextStyle = GoogleFonts.inter( + color: AppColors.disableColor, + ); + + static TextStyle primaryTextStyle = GoogleFonts.inter( + color: AppColors.primaryColor, + ); + + static TextStyle blueTextStyle = GoogleFonts.inter( + color: AppColors.blueColor, + ); + + static TextStyle secondaryTextStyle = GoogleFonts.inter( + color: AppColors.secondaryColor, + ); + + static TextStyle whiteTextStyle = GoogleFonts.inter( + color: AppColors.whiteColor, + ); + + static TextStyle tetriaryTextStyle = GoogleFonts.inter( + color: AppColors.tetriaryColor, + ); + + static TextStyle blackButtonTextStyle = GoogleFonts.inter( + color: AppColors.blackButtonColor, + ); +} + +ThemeData appTheme() { + return ThemeData( + textTheme: GoogleFonts.interTextTheme(), + fontFamily: GoogleFonts.inter().fontFamily, + scaffoldBackgroundColor: AppColors.whiteColor, + ); +} diff --git a/lib/core/widgets/confirm_password_field.dart b/lib/core/widgets/confirm_password_field.dart new file mode 100644 index 0000000..c69d7a6 --- /dev/null +++ b/lib/core/widgets/confirm_password_field.dart @@ -0,0 +1,103 @@ +import 'package:english_learning/features/auth/provider/validator_provider.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ConfirmPasswordFieldWidget extends StatefulWidget { + final String labelText; + final String hintText; + const ConfirmPasswordFieldWidget({ + super.key, + required this.labelText, + required this.hintText, + }); + + @override + _ConfirmPasswordFieldWidgetState createState() => + _ConfirmPasswordFieldWidgetState(); +} + +class _ConfirmPasswordFieldWidgetState + extends State { + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + final validatorProvider = + Provider.of(context, listen: false); + validatorProvider.validateField( + 'confirmPassword', validatorProvider.getError('confirmPassword'), + validator: validatorProvider.confirmPasswordValidator); + } + }); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final validatorProvider = Provider.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.labelText, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + TextField( + focusNode: _focusNode, + obscureText: validatorProvider.isObscure('confirmPassword'), + onChanged: (value) { + validatorProvider.validateField('confirmPassword', value, + validator: validatorProvider.confirmPasswordValidator); + }, + decoration: InputDecoration( + hintText: widget.hintText, + hintStyle: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + ), + suffixIcon: GestureDetector( + onTap: () => + validatorProvider.toggleVisibility('confirmPassword'), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition(scale: animation, child: child); + }, + child: Icon( + validatorProvider.isObscure('confirmPassword') + ? BootstrapIcons.eye_slash + : BootstrapIcons.eye, + color: AppColors.greyColor, + ), + ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + width: 1, + color: AppColors.disableColor, + ), + ), + errorText: validatorProvider.getError('confirmPassword'), + ), + ), + ], + ); + } +} diff --git a/lib/core/widgets/custom_button.dart b/lib/core/widgets/custom_button.dart new file mode 100644 index 0000000..3a25926 --- /dev/null +++ b/lib/core/widgets/custom_button.dart @@ -0,0 +1,59 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:flutter/material.dart'; + +class CustomButton extends StatelessWidget { + final String text; + final double width; + final double height; + final Color color; + final VoidCallback onPressed; + final TextStyle? textStyle; + + const CustomButton({ + super.key, + required this.text, + required this.width, + required this.height, + required this.color, + required this.onPressed, + this.textStyle, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + height: height, + child: DecoratedBox( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + ), + onPressed: onPressed, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + text, + style: textStyle ?? + AppTextStyles.blackButtonTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/email_field.dart b/lib/core/widgets/email_field.dart new file mode 100644 index 0000000..997a487 --- /dev/null +++ b/lib/core/widgets/email_field.dart @@ -0,0 +1,81 @@ +import 'package:english_learning/features/auth/provider/validator_provider.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class EmailFieldWidget extends StatefulWidget { + final String labelText; + final String hintText; + const EmailFieldWidget( + {super.key, required this.labelText, required this.hintText}); + + @override + _EmailFieldWidgetState createState() => _EmailFieldWidgetState(); +} + +class _EmailFieldWidgetState extends State { + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + final validatorProvider = + Provider.of(context, listen: false); + validatorProvider.validateField( + 'email', validatorProvider.getError('email'), + validator: validatorProvider.emailValidator); + } + }); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final validatorProvider = Provider.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.labelText, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + TextField( + focusNode: _focusNode, + keyboardType: TextInputType.emailAddress, + onChanged: (value) { + validatorProvider.validateField('email', value, + validator: validatorProvider.emailValidator); + }, + decoration: InputDecoration( + hintText: widget.hintText, + hintStyle: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + width: 1, + color: AppColors.disableColor, + ), + ), + errorText: validatorProvider.getError('email'), + ), + ), + ], + ); + } +} diff --git a/lib/core/widgets/form_field/class_field.dart b/lib/core/widgets/form_field/class_field.dart new file mode 100644 index 0000000..92ea46b --- /dev/null +++ b/lib/core/widgets/form_field/class_field.dart @@ -0,0 +1,95 @@ +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; + +class ClassFieldWidget extends StatefulWidget { + final bool isRequired; + final String labelText; + final String hintText; + const ClassFieldWidget({ + super.key, + required this.labelText, + required this.hintText, + this.isRequired = false, + }); + + @override + State createState() => _ClassFieldWidgetState(); +} + +class _ClassFieldWidgetState extends State { + String? selectedClass; + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: [ + TextSpan( + text: widget.labelText, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + if (widget.isRequired) + TextSpan( + text: ' *', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + color: AppColors.redColor, // Red color for asterisk + ), + ), + ], + ), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + value: selectedClass, + hint: Text( + widget.hintText, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + ), + ), + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + color: AppColors.disableColor, + width: 1, + ), + ), + ), + icon: const Icon( + BootstrapIcons.chevron_down, + color: AppColors.greyColor, + size: 16, + ), + items: ['Class 1', 'Class 2', 'Class 3', 'Class 4'] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: AppTextStyles.greyTextStyle.copyWith( + fontSize: 12, + ), + ), + ); + }).toList(), + onChanged: (String? newValue) { + setState(() { + selectedClass = newValue; + }); + }, + ), + ], + ); + } +} diff --git a/lib/core/widgets/form_field/custom_field_widget.dart b/lib/core/widgets/form_field/custom_field_widget.dart new file mode 100644 index 0000000..0aa04de --- /dev/null +++ b/lib/core/widgets/form_field/custom_field_widget.dart @@ -0,0 +1,164 @@ +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:english_learning/features/auth/provider/validator_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:provider/provider.dart'; + +class CustomFieldWidget extends StatefulWidget { + final String fieldName; + final String labelText; + final dynamic hintText; + final String? Function(String?)? validator; + final bool obscureText; + final TextEditingController? controller; + final IconButton? suffixIcon; + final TextInputAction textInputAction; + final TextInputType? keyboardType; + final bool? isError; + final FocusNode? focusNode; + final Function(String?)? onChanged; + final String? errorText; + final Function()? onSuffixIconTap; + final bool isRequired; + final bool isEnabled; + + const CustomFieldWidget({ + super.key, + required this.fieldName, + required this.labelText, + required this.textInputAction, + this.hintText, + this.validator, + this.obscureText = false, + this.controller, + this.suffixIcon, + this.focusNode, + this.keyboardType, + this.isError = false, + this.errorText, + this.onChanged, + this.onSuffixIconTap, + this.isRequired = false, + this.isEnabled = true, + }); + + @override + State createState() => _CustomFieldWidgetState(); +} + +class _CustomFieldWidgetState extends State { + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + context + .read() + .setController(widget.fieldName, _controller); + } + + @override + void dispose() { + context.read().removeController(widget.fieldName); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: [ + TextSpan( + text: widget.labelText, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + if (widget.isRequired) + TextSpan( + text: ' *', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + color: AppColors.redColor, // Red color for asterisk + ), + ), + ], + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: widget.controller, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + obscureText: widget.obscureText, + focusNode: widget.focusNode, + onChanged: widget.onChanged, + enabled: widget.isEnabled, + decoration: InputDecoration( + hintText: widget.hintText, + hintStyle: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + ), + fillColor: widget.isEnabled + ? AppColors.whiteColor + : AppColors.cardDisabledColor, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + width: 1, + color: AppColors.cardDisabledColor, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + width: 1, + color: AppColors.cardDisabledColor, + ), + ), + disabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + width: 1, + color: AppColors + .cardDisabledColor, // Adjust this color if necessary + ), + ), + suffixIcon: widget.onSuffixIconTap != null + ? GestureDetector( + onTap: widget.onSuffixIconTap, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: + (Widget child, Animation animation) { + return ScaleTransition(scale: animation, child: child); + }, + child: Icon( + widget.obscureText + ? BootstrapIcons.eye_slash + : BootstrapIcons.eye, + color: AppColors.greyColor, + size: 20, + ), + ), + ) + : null, + errorText: widget.errorText, + ), + validator: widget.validator, + ), + ], + ); + } +} diff --git a/lib/core/widgets/fullname_field.dart b/lib/core/widgets/fullname_field.dart new file mode 100644 index 0000000..245d251 --- /dev/null +++ b/lib/core/widgets/fullname_field.dart @@ -0,0 +1,83 @@ +import 'package:english_learning/features/auth/provider/validator_provider.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class FullNameFieldWidget extends StatefulWidget { + final String labelText; + final String hintText; + const FullNameFieldWidget({ + super.key, + required this.labelText, + required this.hintText, + }); + + @override + _FullNameFieldWidgetState createState() => _FullNameFieldWidgetState(); +} + +class _FullNameFieldWidgetState extends State { + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + final validatorProvider = + Provider.of(context, listen: false); + validatorProvider.validateField( + 'fullName', validatorProvider.getError('fullName'), + validator: validatorProvider.fullNameValidator); + } + }); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final validatorProvider = Provider.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.labelText, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + TextField( + focusNode: _focusNode, + onChanged: (value) { + validatorProvider.validateField('fullName', value, + validator: validatorProvider.fullNameValidator); + }, + decoration: InputDecoration( + hintText: widget.hintText, + hintStyle: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + width: 1, + color: AppColors.disableColor, + ), + ), + errorText: validatorProvider.getError('fullName'), + ), + ), + ], + ); + } +} diff --git a/lib/core/widgets/global_button.dart b/lib/core/widgets/global_button.dart new file mode 100644 index 0000000..f6a3cb6 --- /dev/null +++ b/lib/core/widgets/global_button.dart @@ -0,0 +1,109 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:flutter/material.dart'; + +class GlobalButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final double? width; + final double? height; + final IconData? icon; + final double? iconSize; + final bool spaceBetween; + final Gradient? gradient; + final Color? textColor; + final Color? backgroundColor; + final Color? borderColor; + final double borderWidth; + final bool transparentBackground; + + const GlobalButton({ + super.key, + required this.text, + required this.onPressed, + this.width = double.infinity, + this.height = 49.0, + this.icon, + this.iconSize = 24.0, + this.spaceBetween = false, + this.gradient, + this.textColor = Colors.white, + this.backgroundColor, + this.borderColor, + this.borderWidth = 1.0, + this.transparentBackground = false, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + height: height, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: transparentBackground + ? null + : (gradient ?? + (backgroundColor != null ? null : AppColors.gradientTheme)), + color: gradient == null + ? backgroundColor + : null, // Use solid color if no gradient + borderRadius: BorderRadius.circular(12), + border: borderColor != null + ? Border.all(color: borderColor!, width: borderWidth) + : null, + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + ), + ), + onPressed: onPressed, + child: spaceBetween && icon != null + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + text, + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.bold, + color: textColor, + ), + ), + Icon( + icon, + size: iconSize, + color: textColor ?? Colors.white, + ), + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + text, + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.bold, + color: textColor, + ), + ), + if (icon != null) + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Icon( + icon, + size: iconSize, + color: textColor ?? Colors.white, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/nisn_field.dart b/lib/core/widgets/nisn_field.dart new file mode 100644 index 0000000..aaa4de5 --- /dev/null +++ b/lib/core/widgets/nisn_field.dart @@ -0,0 +1,84 @@ +import 'package:english_learning/features/auth/provider/validator_provider.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class NISNFieldWidget extends StatefulWidget { + final String labelText; + final String hintText; + const NISNFieldWidget({ + super.key, + required this.labelText, + required this.hintText, + }); + + @override + _NISNFieldWidgetState createState() => _NISNFieldWidgetState(); +} + +class _NISNFieldWidgetState extends State { + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + final validatorProvider = + Provider.of(context, listen: false); + validatorProvider.validateField( + 'nisn', validatorProvider.getError('nisn'), + validator: validatorProvider.nisnValidator); + } + }); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final validatorProvider = Provider.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.labelText, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + TextField( + keyboardType: TextInputType.number, + focusNode: _focusNode, + onChanged: (value) { + validatorProvider.validateField('nisn', value, + validator: validatorProvider.nisnValidator); + }, + decoration: InputDecoration( + hintText: widget.hintText, + hintStyle: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + width: 1, + color: AppColors.disableColor, + ), + ), + errorText: validatorProvider.getError('nisn'), + ), + ), + ], + ); + } +} diff --git a/lib/core/widgets/password_field.dart b/lib/core/widgets/password_field.dart new file mode 100644 index 0000000..2334827 --- /dev/null +++ b/lib/core/widgets/password_field.dart @@ -0,0 +1,100 @@ +import 'package:english_learning/features/auth/provider/validator_provider.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class PasswordFieldWidget extends StatefulWidget { + final String hintText; + final String labelText; + const PasswordFieldWidget({ + super.key, + required this.hintText, + required this.labelText, + }); + + @override + _PasswordFieldWidgetState createState() => _PasswordFieldWidgetState(); +} + +class _PasswordFieldWidgetState extends State { + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + final validatorProvider = + Provider.of(context, listen: false); + validatorProvider.validateField( + 'password', validatorProvider.getError('password'), + validator: validatorProvider.passwordValidator); + } + }); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final validatorProvider = Provider.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.labelText, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + TextField( + focusNode: _focusNode, + obscureText: validatorProvider.isObscure('password'), + onChanged: (value) { + validatorProvider.validateField('password', value, + validator: validatorProvider.passwordValidator); + }, + decoration: InputDecoration( + hintText: widget.hintText, + hintStyle: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + ), + suffixIcon: GestureDetector( + onTap: () => validatorProvider.toggleVisibility('password'), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition(scale: animation, child: child); + }, + child: Icon( + validatorProvider.isObscure('password') + ? BootstrapIcons.eye_slash + : BootstrapIcons.eye, + color: AppColors.greyColor, + ), + ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + width: 1, + color: AppColors.disableColor, + ), + ), + errorText: validatorProvider.getError('password'), + ), + ), + ], + ); + } +} diff --git a/lib/core/widgets/slider_widget.dart b/lib/core/widgets/slider_widget.dart new file mode 100644 index 0000000..fb900cc --- /dev/null +++ b/lib/core/widgets/slider_widget.dart @@ -0,0 +1,40 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:flutter/material.dart'; + +class SliderWidget extends StatelessWidget { + final int currentPage; + final int itemCount; + + const SliderWidget({ + super.key, + required this.currentPage, + required this.itemCount, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + itemCount, + (index) => AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.symmetric(horizontal: 4.0), + height: 8, + width: currentPage == index ? 24 : 8, + decoration: BoxDecoration( + gradient: currentPage == index + ? AppColors.gradientTheme + : const LinearGradient( + colors: [ + AppColors.sliderInActive, + AppColors.sliderInActive, + ], + ), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ); + } +} diff --git a/lib/features/auth/assets/images/forgot_password_illustration.svg b/lib/features/auth/assets/images/forgot_password_illustration.svg new file mode 100644 index 0000000..c23dc1b --- /dev/null +++ b/lib/features/auth/assets/images/forgot_password_illustration.svg @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/auth/assets/images/login_illustration.svg b/lib/features/auth/assets/images/login_illustration.svg new file mode 100644 index 0000000..01068e0 --- /dev/null +++ b/lib/features/auth/assets/images/login_illustration.svg @@ -0,0 +1,284 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/auth/assets/images/shrug.png b/lib/features/auth/assets/images/shrug.png new file mode 100644 index 0000000..12e8659 Binary files /dev/null and b/lib/features/auth/assets/images/shrug.png differ diff --git a/lib/features/auth/assets/images/signup_verification.svg b/lib/features/auth/assets/images/signup_verification.svg new file mode 100644 index 0000000..25089c6 --- /dev/null +++ b/lib/features/auth/assets/images/signup_verification.svg @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/auth/provider/user_provider.dart b/lib/features/auth/provider/user_provider.dart new file mode 100644 index 0000000..0c33162 --- /dev/null +++ b/lib/features/auth/provider/user_provider.dart @@ -0,0 +1,290 @@ +import 'dart:io'; +import 'package:english_learning/core/services/repositories/user_repository.dart'; +import 'package:flutter/material.dart'; +import 'package:jwt_decoder/jwt_decoder.dart'; + +class UserProvider with ChangeNotifier { + final UserRepository _userRepository = UserRepository(); + bool _isLoggedIn = false; + String? _jwtToken; + Map? _userData; + File? _selectedImage; + + bool get isLoggedIn => _isLoggedIn; + String? get jwtToken => _jwtToken; + Map? get userData => _userData; + File? get selectedImage => _selectedImage; + + UserProvider() { + _loadLoginStatus(); + } + + Future _loadLoginStatus() async { + try { + _jwtToken = await _userRepository.getToken(); + if (_jwtToken != null) { + if (JwtDecoder.isExpired(_jwtToken!)) { + await logout(); + } else { + _isLoggedIn = true; + await _loadUserData(); + } + } + } catch (e) { + print('Error loading login status: $e'); + } + notifyListeners(); + } + + Future _loadUserData() async { + _userData = await _userRepository.getUserData(); + if (_userData == null) { + await refreshUserData(); + } + } + + Future refreshUserData() async { + try { + if (_jwtToken != null) { + final response = await _userRepository.getMe(_jwtToken!); + if (response.statusCode == 200) { + _userData = response.data['payload']; + // Save all user data securely + await _userRepository.saveUserData(_userData!); + notifyListeners(); + } + } + } catch (e) { + print('Error refreshing user data: $e'); + } + } + + Future login({required String email, required String password}) async { + try { + final response = await _userRepository.loginUser({ + 'EMAIL': email, + 'PASSWORD': password, + }); + + if (response.statusCode == 200 && + response.data['payload']['TOKEN'] != null) { + String token = response.data['payload']['TOKEN']; + String refreshToken = response.data['payload']['REFRESH_TOKEN']; + if (token.startsWith('Bearer ')) { + token = token.substring(7); + } + _jwtToken = token; + await _userRepository.saveToken(_jwtToken!); + await _userRepository.saveRefreshToken(refreshToken); + _isLoggedIn = true; + await refreshUserData(); + return true; + } + return false; + } catch (e) { + print('Login error: $e'); + return false; + } + } + + Future register({ + required String name, + required String email, + required String nisn, + required String password, + required String confirmPassword, + }) async { + try { + final response = await _userRepository.registerUser({ + "NAME_USERS": name, + "EMAIL": email, + "NISN": nisn, + "PASSWORD": password, + "CONFIRM_PASSWORD": confirmPassword, + }); + + if (response.statusCode == 200) { + return true; + } + return false; + } catch (e) { + print('Registration error: $e'); + return false; + } + } + + Future logout() async { + try { + final response = await _userRepository.logoutUser(); + + if (response.statusCode == 200) { + _isLoggedIn = false; + _jwtToken = null; + _userData = null; + await _userRepository.deleteToken(); + await _userRepository.deleteRefreshToken(); + await _userRepository.deleteUserData(); + notifyListeners(); + return true; + } + return false; + } catch (e) { + print('Logout error: $e'); + _isLoggedIn = false; + _jwtToken = null; + _userData = null; + await _userRepository.deleteToken(); + await _userRepository.deleteRefreshToken(); + await _userRepository.deleteUserData(); + notifyListeners(); + return false; + } + } + + Future forgotPassword({required String email}) async { + try { + final response = await _userRepository.forgotPassword(email); + + if (response.statusCode == 200) { + print("Password reset email sent successfully!"); + return true; + } else if (response.statusCode == 404) { + print("Email is not registered!"); + return false; + } + return false; + } catch (e) { + print('Forgot Password error: $e'); + return false; + } + } + + Future refreshToken() async { + try { + final refreshToken = await _userRepository.getRefreshToken(); + if (refreshToken == null) return false; + + final response = + await _userRepository.dioClient.refreshAccessToken(refreshToken); + + if (response.statusCode == 200) { + String newToken = response.data['payload']['TOKEN']; + String newRefreshToken = response.data['payload']['REFRESH_TOKEN']; + + if (newToken.startsWith('Bearer ')) { + newToken = newToken.substring(7); + } + + _jwtToken = newToken; + await _userRepository.saveToken(_jwtToken!); + await _userRepository.saveRefreshToken(newRefreshToken); + + return true; + } + return false; + } catch (e) { + print('Error refreshing token: $e'); + return false; + } + } + + Future isTokenValid() async { + if (_jwtToken == null) return false; + if (JwtDecoder.isExpired(_jwtToken!)) { + return await refreshToken(); + } + return true; + } + + Future getValidToken() async { + try { + if (await isTokenValid()) { + return _jwtToken; + } + return null; + } catch (e) { + print('Error in getValidToken: $e'); + return null; + } + } + + Future updateUserProfile(Map updatedData) async { + try { + final token = await getValidToken(); + final userId = _userData?['ID']; + if (token != null && userId != null) { + final response = await _userRepository.updateUserProfile( + userId, + updatedData, + token, + imageFile: _selectedImage, + ); + if (response.statusCode == 200) { + _userData = response.data['payload']; + // Save updated user data securely + await _userRepository.saveUserData(_userData!); + _selectedImage = + null; // Reset the selected image after successful update + notifyListeners(); + } + } + } catch (e) { + print('Error updating user profile: $e'); + rethrow; + } + } + + Future updatePassword({ + required String oldPassword, + required String newPassword, + required String confirmPassword, + }) async { + try { + final token = await getValidToken(); + final userId = _userData?['ID']; + if (token != null && userId != null) { + final response = await _userRepository.updatePassword( + userId, + oldPassword, + newPassword, + confirmPassword, + token, + ); + if (response.statusCode == 200) { + print("Password updated successfully!"); + return true; + } + } + return false; + } catch (e) { + print('Error updating password: $e'); + return false; + } + } + + Future reportIssue(String report) async { + try { + final token = await getValidToken(); + if (token != null) { + final response = await _userRepository.reportIssue(report, token); + if (response.statusCode == 201) { + print("Issue reported successfully!"); + return true; + } + } + return false; + } catch (e) { + print('Error reporting issue: $e'); + return false; + } + } + + String? getUserName() { + return _userData?['NAME_USERS']; + } + + void setSelectedImage(File? image) { + _selectedImage = image; + notifyListeners(); + } +} diff --git a/lib/features/auth/provider/validator_provider.dart b/lib/features/auth/provider/validator_provider.dart new file mode 100644 index 0000000..171a8ac --- /dev/null +++ b/lib/features/auth/provider/validator_provider.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; + +class ValidatorProvider extends ChangeNotifier { + final Map _errors = {}; + final Map _obscureFields = {}; + final Map _focusNodes = {}; + final Map _controllers = {}; + String? _password; + + bool validateAllFields() { + bool isValid = true; + + // Validasi email + final emailError = emailValidator(_controllers['email']?.text); + _errors['email'] = emailError; + if (emailError != null) isValid = false; + + // Validasi nama lengkap + final fullNameError = fullNameValidator(_controllers['fullname']?.text); + _errors['fullname'] = fullNameError; + if (fullNameError != null) isValid = false; + + // Validasi password (jika ada) + if (_controllers.containsKey('password')) { + final passwordError = passwordValidator(_controllers['password']?.text); + _errors['password'] = passwordError; + if (passwordError != null) isValid = false; + } + + // Validasi konfirmasi password (jika ada) + if (_controllers.containsKey('confirmPassword')) { + final confirmPasswordError = + confirmPasswordValidator(_controllers['confirmPassword']?.text); + _errors['confirmPassword'] = confirmPasswordError; + if (confirmPasswordError != null) isValid = false; + } + + // Validasi NISN (jika ada) + if (_controllers.containsKey('nisn')) { + final nisnError = nisnValidator(_controllers['nisn']?.text); + _errors['nisn'] = nisnError; + if (nisnError != null) isValid = false; + } + + notifyListeners(); + return isValid; + } + + // Mengambil error untuk field tertentu + String? getError(String field) => _errors[field]; + + // Memeriksa apakah field tertentu sedang disembunyikan (obscure) + bool isObscure(String field) => _obscureFields[field] ?? true; + + // Mengambil FocusNode untuk field tertentu + FocusNode? getFocusNode(String field) => _focusNodes[field]; + + // Toggle visibility untuk field tertentu + void toggleVisibility(String field) { + _obscureFields[field] = !(isObscure(field)); + notifyListeners(); + } + + // Mengatur FocusNode untuk field tertentu + void setFocusNode(String field, FocusNode focusNode) { + _focusNodes[field] = focusNode; + notifyListeners(); + } + + // Validasi field tertentu dengan validator yang diberikan + void validateField(String field, String? value, + {String? Function(String?)? validator}) { + // Jika validator tidak null, gunakan untuk memvalidasi nilai field + final error = validator != null ? validator(value) : null; + _errors[field] = error; + + // Simpan password jika field adalah 'password' + if (field == 'password') { + _password = value; + } + + // Validasi confirm password jika field adalah 'confirmPassword' + if (field == 'confirmPassword') { + _errors[field] = confirmPasswordValidator(value); + } + + notifyListeners(); + } + + // Reset the fields and clear errors + void resetFields() { + _errors.clear(); + _obscureFields.clear(); + _password = null; + + for (final controller in _controllers.values) { + controller.clear(); + } + + notifyListeners(); + } + + // Set TextEditingController for a field + void setController(String field, TextEditingController controller) { + _controllers[field] = controller; + } + + // Remove TextEditingController when not needed anymore + void removeController(String field) { + _controllers.remove(field); + } + + @override + void dispose() { + // Dispose controllers and focus nodes + for (final controller in _controllers.values) { + controller.dispose(); + } + for (final focusNode in _focusNodes.values) { + focusNode?.dispose(); + } + _controllers.clear(); + _focusNodes.clear(); + super.dispose(); + } + + // Validator email + String? emailValidator(String? value) { + if (value == null || value.isEmpty) { + return 'Email cannot be empty'; + } + const emailRegex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$"; + if (!RegExp(emailRegex).hasMatch(value)) { + return 'Invalid email format (e.g. example@gmail.com)'; + } + return null; + } + + // Validator password + String? passwordValidator(String? value) { + if (value == null || value.isEmpty) { + return 'Password cannot be empty'; + } else if (value.length < 8) { + return 'Password must be at least 8 characters long'; + } + _password = value; + return null; + } + + // Validator untuk konfirmasi password + String? confirmPasswordValidator(String? value) { + if (value == null || value.isEmpty) { + return 'Please confirm your password'; + } else if (value != _password) { + return 'Passwords do not match'; + } + return null; + } + + // Validator NISN + String? nisnValidator(String? value) { + if (value == null || value.isEmpty) { + return 'NISN cannot be empty'; + } else if (value.length != 10 || !RegExp(r'^\d{10}$').hasMatch(value)) { + return 'NISN must be exactly 10 digits'; + } + return null; + } + + // Validator nama lengkap + String? fullNameValidator(String? value) { + if (value == null || value.isEmpty) { + return 'Full name cannot be empty'; + } + return null; + } + + // Menghapus error dari field tertentu + void clearError(String field) { + _errors[field] = null; + notifyListeners(); + } + + // Menghapus semua error + void clearErrors() { + _errors.clear(); + notifyListeners(); + } + + // Menghapus FocusNode saat tidak diperlukan lagi + void disposeFocusNodes() { + for (final focusNode in _focusNodes.values) { + focusNode?.dispose(); + } + _focusNodes.clear(); + notifyListeners(); + } +} diff --git a/lib/features/auth/screens/forgot_password/forgot_password_screen.dart b/lib/features/auth/screens/forgot_password/forgot_password_screen.dart new file mode 100644 index 0000000..843d8fe --- /dev/null +++ b/lib/features/auth/screens/forgot_password/forgot_password_screen.dart @@ -0,0 +1,171 @@ +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/auth/provider/validator_provider.dart'; +import 'package:english_learning/features/auth/screens/signin/signin_screen.dart'; +import 'package:english_learning/core/widgets/form_field/custom_field_widget.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/auth/widgets/dialog/forgot_password_verification.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ForgotPasswordScreen extends StatefulWidget { + const ForgotPasswordScreen({super.key}); + + @override + State createState() => _ForgotPasswordScreenState(); +} + +class _ForgotPasswordScreenState extends State { + final TextEditingController _emailController = TextEditingController(); + bool _isLoading = false; + Future _handleForgotPassword() async { + setState(() { + _isLoading = true; + }); + + final userProvider = Provider.of(context, listen: false); + bool success = await userProvider.forgotPassword( + email: _emailController.text, + ); + + setState(() { + _isLoading = false; + }); + + if (success) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return ForgotPasswordVerification( + onSubmit: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SigninScreen(), + ), + ); + }, + ); + }, + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Email is not registered!'), + backgroundColor: AppColors.redColor, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final mediaQuery = MediaQuery.of(context); + final screenHeight = mediaQuery.size.height; + + return Scaffold( + backgroundColor: AppColors.whiteColor, + body: SingleChildScrollView( + child: Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Consumer( + builder: (context, validatorProvider, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: screenHeight * 0.15), + Text( + 'Forgot Password?', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 30, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 4), + Text( + 'Enter your email and we will send you a link to reset your password', + style: AppTextStyles.tetriaryTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: screenHeight * 0.05), + const Center( + child: Image( + image: AssetImage( + 'lib/features/auth/assets/images/shrug.png'), + width: 200, + height: 205.37, + ), + ), + SizedBox(height: screenHeight * 0.08), + CustomFieldWidget( + controller: _emailController, + fieldName: 'email', + isRequired: true, + textInputAction: TextInputAction.next, + labelText: 'Email Address', + hintText: 'Enter Your Email Address', + keyboardType: TextInputType.emailAddress, + validator: validatorProvider.emailValidator, + onChanged: (value) { + validatorProvider.validateField( + 'email', + value, + validator: validatorProvider.emailValidator, + ); + }, + errorText: validatorProvider.getError('email'), + ), + const SizedBox(height: 24), + _isLoading + ? const Center(child: CircularProgressIndicator()) + : GlobalButton( + text: 'Submit', + onPressed: _handleForgotPassword, + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Remember the password? ', + style: AppTextStyles.blackButtonTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SigninScreen()), + ); + }, + child: Text( + 'Login', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ], + ), + ], + ); + }), + ), + ), + ), + ); + } +} diff --git a/lib/features/auth/screens/login_screen.dart b/lib/features/auth/screens/login_screen.dart deleted file mode 100644 index 59c50b6..0000000 --- a/lib/features/auth/screens/login_screen.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -class LoginScreen extends StatefulWidget { - const LoginScreen({super.key}); - - @override - State createState() => _LoginScreenState(); -} - -class _LoginScreenState extends State { - @override - Widget build(BuildContext context) { - return const Scaffold( - body: Center(child: Text('Login Screen')), - ); - } -} diff --git a/lib/features/auth/screens/signin/signin_screen.dart b/lib/features/auth/screens/signin/signin_screen.dart new file mode 100644 index 0000000..8241a63 --- /dev/null +++ b/lib/features/auth/screens/signin/signin_screen.dart @@ -0,0 +1,215 @@ +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/auth/provider/validator_provider.dart'; +import 'package:english_learning/features/auth/screens/forgot_password/forgot_password_screen.dart'; +import 'package:english_learning/features/auth/screens/signup/signup_screen.dart'; +import 'package:english_learning/features/home/screens/home_screen.dart'; +import 'package:english_learning/core/widgets/form_field/custom_field_widget.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; + +class SigninScreen extends StatelessWidget { + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + + SigninScreen({super.key}); + + @override + Widget build(BuildContext context) { + final userProvider = Provider.of(context, listen: false); + Provider.of(context, listen: false); + + return Scaffold( + backgroundColor: AppColors.whiteColor, + body: Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: Consumer( + builder: (context, validatorProvider, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 32), + Text( + 'Welcome Back!', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 30, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 4), + Text( + 'Login to continue your personalized learning journey.', + style: AppTextStyles.greyTextStyle.copyWith( + fontSize: 14, + ), + ), + const SizedBox(height: 26), + Center( + child: SvgPicture.asset( + 'lib/features/auth/assets/images/login_illustration.svg', + width: 200, + ), + ), + const SizedBox(height: 30), + CustomFieldWidget( + fieldName: 'email', + controller: _emailController, + isRequired: true, + textInputAction: TextInputAction.next, + labelText: 'Email Address', + hintText: 'Enter Your Email Address', + keyboardType: TextInputType.emailAddress, + validator: validatorProvider.emailValidator, + onChanged: (value) { + validatorProvider.validateField( + 'email', + value, + validator: validatorProvider.emailValidator, + ); + }, + errorText: validatorProvider.getError('email'), + ), + const SizedBox(height: 14), + CustomFieldWidget( + fieldName: 'password', + controller: _passwordController, + isRequired: true, + textInputAction: TextInputAction.next, + labelText: 'Password', + hintText: 'Create a Strong Password', + obscureText: validatorProvider.isObscure('password'), + keyboardType: TextInputType.visiblePassword, + validator: validatorProvider.passwordValidator, + onChanged: (value) { + validatorProvider.validateField( + 'password', + value, + validator: validatorProvider.passwordValidator, + ); + }, + onSuffixIconTap: () => + validatorProvider.toggleVisibility('password'), + errorText: validatorProvider.getError('password'), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const ForgotPasswordScreen()), + ); + }, + child: ShaderMask( + shaderCallback: (bounds) => + AppColors.gradientTheme.createShader( + Rect.fromLTWH(0, 0, bounds.width, bounds.height), + ), + child: const Text( + 'Forgot Password?', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Login', + onPressed: () async { + // Validate email and password fields + validatorProvider.validateField( + 'email', + _emailController.text, + validator: validatorProvider.emailValidator, + ); + validatorProvider.validateField( + 'password', + _passwordController.text, + validator: validatorProvider.passwordValidator, + ); + + // If no errors, proceed with login + if (validatorProvider.getError('email') == null && + validatorProvider.getError('password') == null) { + final isSuccess = await userProvider.login( + email: _emailController.text, + password: _passwordController.text, + ); + + if (isSuccess) { + // Navigate to HomeScreen after successful login + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => const HomeScreen()), + (Route route) => + false, // Remove all previous routes + ).then((_) { + // Reset the fields after login + validatorProvider.resetFields(); + _emailController.clear(); + _passwordController.clear(); + }); + } else { + // Show error message + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Login failed, please check your credentials'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Haven\'t joined us yet? ', + style: AppTextStyles.blackTextStyle + .copyWith(fontSize: 14), + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SignupScreen()), + ); + }, + child: Text( + 'Sign Up Here', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ], + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/auth/screens/signin/widgets/login_email_field.dart b/lib/features/auth/screens/signin/widgets/login_email_field.dart new file mode 100644 index 0000000..e0d938a --- /dev/null +++ b/lib/features/auth/screens/signin/widgets/login_email_field.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/auth/provider/validator_provider.dart'; + +class LoginEmailField extends StatefulWidget { + const LoginEmailField({super.key}); + + @override + _LoginEmailFieldState createState() => _LoginEmailFieldState(); +} + +class _LoginEmailFieldState extends State { + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + final validatorProvider = + Provider.of(context, listen: false); + validatorProvider.validateField( + 'email', validatorProvider.getError('email'), + validator: validatorProvider.emailValidator); + } + }); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final validatorProvider = Provider.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Email', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + TextField( + focusNode: _focusNode, + keyboardType: TextInputType.emailAddress, + onChanged: (value) { + validatorProvider.validateField('email', value, + validator: validatorProvider.emailValidator); + }, + decoration: InputDecoration( + hintText: 'Enter Your Email Address', + hintStyle: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + color: AppColors.disableColor, + width: 1, + ), + ), + errorText: validatorProvider.getError('email'), + ), + ) + ], + ); + } +} diff --git a/lib/features/auth/screens/signin/widgets/login_password_field.dart b/lib/features/auth/screens/signin/widgets/login_password_field.dart new file mode 100644 index 0000000..fca09ee --- /dev/null +++ b/lib/features/auth/screens/signin/widgets/login_password_field.dart @@ -0,0 +1,93 @@ +import 'package:english_learning/features/auth/provider/validator_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:provider/provider.dart'; + +class LoginPasswordField extends StatefulWidget { + const LoginPasswordField({super.key}); + + @override + _LoginPasswordFieldState createState() => _LoginPasswordFieldState(); +} + +class _LoginPasswordFieldState extends State { + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + final validatorProvider = + Provider.of(context, listen: false); + validatorProvider.validateField( + 'password', validatorProvider.getError('password'), + validator: validatorProvider.passwordValidator); + } + }); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final validatorProvider = Provider.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Password', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + TextField( + focusNode: _focusNode, + obscureText: validatorProvider.isObscure('password'), + onChanged: (value) { + validatorProvider.validateField('password', value, + validator: validatorProvider.passwordValidator); + }, + decoration: InputDecoration( + hintText: 'Enter Your Password', + hintStyle: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + ), + suffixIcon: GestureDetector( + onTap: () => validatorProvider.toggleVisibility('password'), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition(scale: animation, child: child); + }, + child: Icon( + validatorProvider.isObscure('password') + ? BootstrapIcons.eye_slash + : BootstrapIcons.eye, + color: AppColors.greyColor, + ), + ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + width: 1, + color: AppColors.disableColor, + ), + ), + errorText: validatorProvider.getError('password'), + ), + ), + ], + ); + } +} diff --git a/lib/features/auth/screens/signup/signup_screen.dart b/lib/features/auth/screens/signup/signup_screen.dart new file mode 100644 index 0000000..4793622 --- /dev/null +++ b/lib/features/auth/screens/signup/signup_screen.dart @@ -0,0 +1,257 @@ +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/auth/provider/validator_provider.dart'; +import 'package:english_learning/features/auth/screens/signin/signin_screen.dart'; +import 'package:english_learning/core/widgets/form_field/custom_field_widget.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/auth/widgets/dialog/signup_verification.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class SignupScreen extends StatelessWidget { + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _nisnController = TextEditingController(); + final TextEditingController _nameController = TextEditingController(); + + SignupScreen({super.key}); + + @override + Widget build(BuildContext context) { + final userProvider = Provider.of(context, listen: false); + Provider.of(context, listen: false); + + final mediaQuery = MediaQuery.of(context); + final screenHeight = mediaQuery.size.height; + + return Scaffold( + backgroundColor: AppColors.whiteColor, + body: Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: Consumer( + builder: (context, validatorProvider, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: screenHeight * 0.05), + Text( + '👋 Hi!, you here!', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 24, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 4), + Text( + 'Let\'s start by creating your account first', + style: AppTextStyles.greyTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 49), + CustomFieldWidget( + controller: _nisnController, + fieldName: 'nisn', + isRequired: true, + textInputAction: TextInputAction.next, + labelText: 'NISN', + hintText: 'Enter Your NISN', + keyboardType: TextInputType.number, + validator: validatorProvider.nisnValidator, + onChanged: (value) { + validatorProvider.validateField('nisn', value, + validator: validatorProvider.nisnValidator); + }, + errorText: validatorProvider.getError('nisn'), + ), + const SizedBox(height: 14), + CustomFieldWidget( + controller: _nameController, + fieldName: 'name', + isRequired: true, + textInputAction: TextInputAction.next, + labelText: 'Full Name', + hintText: 'Enter Your Full Name', + keyboardType: TextInputType.text, + validator: validatorProvider.fullNameValidator, + onChanged: (value) { + validatorProvider.validateField( + 'fullname', + value, + validator: validatorProvider.fullNameValidator, + ); + }, + errorText: validatorProvider.getError('name'), + ), + const SizedBox(height: 14), + CustomFieldWidget( + controller: _emailController, + fieldName: 'email', + isRequired: true, + textInputAction: TextInputAction.next, + labelText: 'Email Address', + hintText: 'Enter Your Email Address', + keyboardType: TextInputType.emailAddress, + validator: validatorProvider.emailValidator, + onChanged: (value) { + validatorProvider.validateField( + 'email', + value, + validator: validatorProvider.emailValidator, + ); + }, + errorText: validatorProvider.getError('email'), + ), + const SizedBox(height: 14), + CustomFieldWidget( + controller: _passwordController, + fieldName: 'password', + isRequired: true, + textInputAction: TextInputAction.next, + labelText: 'Password', + hintText: 'Create a Strong Password', + obscureText: validatorProvider.isObscure('password'), + keyboardType: TextInputType.visiblePassword, + validator: validatorProvider.passwordValidator, + onChanged: (value) { + validatorProvider.validateField( + 'password', + value, + validator: validatorProvider.passwordValidator, + ); + }, + onSuffixIconTap: () => + validatorProvider.toggleVisibility('password'), + errorText: validatorProvider.getError('password'), + ), + const SizedBox(height: 14), + CustomFieldWidget( + fieldName: 'confirmPassword', + isRequired: true, + textInputAction: TextInputAction.next, + labelText: 'Confirm Password', + hintText: 'Retype Your Password', + obscureText: + validatorProvider.isObscure('confirmPassword'), + keyboardType: TextInputType.visiblePassword, + validator: validatorProvider.confirmPasswordValidator, + onChanged: (value) { + validatorProvider.validateField( + 'confirmPassword', + value, + validator: validatorProvider.confirmPasswordValidator, + ); + }, + onSuffixIconTap: () => + validatorProvider.toggleVisibility('confirmPassword'), + errorText: validatorProvider.getError('confirmPassword'), + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Sign Up', + onPressed: () async { + // Validate all fields + validatorProvider.validateField( + 'nisn', _nisnController.text, + validator: validatorProvider.nisnValidator); + validatorProvider.validateField( + 'name', _nameController.text, + validator: validatorProvider.fullNameValidator); + validatorProvider.validateField( + 'email', _emailController.text, + validator: validatorProvider.emailValidator); + validatorProvider.validateField( + 'password', _passwordController.text, + validator: validatorProvider.passwordValidator); + validatorProvider.validateField( + 'confirmPassword', _passwordController.text, + validator: + validatorProvider.confirmPasswordValidator); + + // If no error, proceed with registration + if (validatorProvider.getError('nisn') == null && + validatorProvider.getError('name') == null && + validatorProvider.getError('email') == null && + validatorProvider.getError('password') == null && + validatorProvider.getError('confirmPassword') == + null) { + // Call the register method with required data + final isSuccess = await userProvider.register( + name: _nameController.text, + email: _emailController.text, + nisn: _nisnController.text, + password: _passwordController.text, + confirmPassword: _passwordController.text, + ); + + if (isSuccess) { + // Show success dialog + showDialog( + context: context, + builder: (BuildContext context) { + return SignupVerification( + onSubmit: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SigninScreen(), + ), + ); + }, + ); + }, + ); + } else { + // Show error message + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Registration failed'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Already have an account?', + style: AppTextStyles.blackTextStyle + .copyWith(fontSize: 14), + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SigninScreen()), + ).then((_) { + context.read().resetFields(); + }); + }, + child: Text( + ' Login Here', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w900, + ), + ), + ), + ], + ), + ], + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/auth/screens/signup_screen.dart b/lib/features/auth/screens/signup_screen.dart deleted file mode 100644 index 566bcac..0000000 --- a/lib/features/auth/screens/signup_screen.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -class SignupScreen extends StatefulWidget { - const SignupScreen({super.key}); - - @override - State createState() => _SignupScreenState(); -} - -class _SignupScreenState extends State { - @override - Widget build(BuildContext context) { - return const Scaffold( - body: Center(child: Text('Signup Screen')), - ); - } -} diff --git a/lib/features/auth/widgets/dialog/forgot_password_verification.dart b/lib/features/auth/widgets/dialog/forgot_password_verification.dart new file mode 100644 index 0000000..5b9d615 --- /dev/null +++ b/lib/features/auth/widgets/dialog/forgot_password_verification.dart @@ -0,0 +1,83 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ForgotPasswordVerification extends StatelessWidget { + final VoidCallback onSubmit; + + const ForgotPasswordVerification({ + super.key, + required this.onSubmit, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Check Your', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + TextSpan( + text: ' Email', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + TextSpan( + text: '!', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + SvgPicture.asset( + 'lib/features/auth/assets/images/forgot_password_illustration.svg', + width: 160, + ), + const SizedBox(height: 16), + Text( + 'We\'ve sent a link to your email to help you reset your password. Please check your inbox to continue..', + textAlign: TextAlign.center, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Okay', + onPressed: onSubmit, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/auth/widgets/dialog/signup_verification.dart b/lib/features/auth/widgets/dialog/signup_verification.dart new file mode 100644 index 0000000..be01540 --- /dev/null +++ b/lib/features/auth/widgets/dialog/signup_verification.dart @@ -0,0 +1,76 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class SignupVerification extends StatelessWidget { + final VoidCallback onSubmit; + + const SignupVerification({ + super.key, + required this.onSubmit, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Account Verification', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + TextSpan( + text: ' in Progress', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + SvgPicture.asset( + 'lib/features/auth/assets/images/signup_verification.svg', + width: 160, + ), + const SizedBox(height: 16), + Text( + 'Your Registration is complete, but we need to verify your details. We\'ll notify you once your account is ready.', + textAlign: TextAlign.center, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Okay', + onPressed: onSubmit, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/history/assets/images/is_empty_illustration.svg b/lib/features/history/assets/images/is_empty_illustration.svg new file mode 100644 index 0000000..07c1738 --- /dev/null +++ b/lib/features/history/assets/images/is_empty_illustration.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/history/models/history_model.dart b/lib/features/history/models/history_model.dart new file mode 100644 index 0000000..a23dfb6 --- /dev/null +++ b/lib/features/history/models/history_model.dart @@ -0,0 +1,34 @@ +import 'package:intl/intl.dart'; + +class LearningHistory { + final int score; + final dynamic currentLevel; + final dynamic nextLevel; + final DateTime studentFinish; + final String topicName; + final String sectionName; + + LearningHistory({ + required this.score, + required this.currentLevel, + required this.nextLevel, + required this.studentFinish, + required this.topicName, + required this.sectionName, + }); + + factory LearningHistory.fromJson(Map json) { + return LearningHistory( + score: json['SCORE'], + currentLevel: json['CURRENT_LEVEL'], + nextLevel: json['NEXT_LEVEL'], + studentFinish: DateTime.parse(json['STUDENT_FINISH']), + topicName: json['TOPIC_NAME'], + sectionName: json['SECTION_NAME'], + ); + } + + String get formattedDate { + return DateFormat('yyyy-MM-dd HH:mm').format(studentFinish); + } +} diff --git a/lib/features/history/provider/history_provider.dart b/lib/features/history/provider/history_provider.dart new file mode 100644 index 0000000..ead5fea --- /dev/null +++ b/lib/features/history/provider/history_provider.dart @@ -0,0 +1,86 @@ +import 'package:english_learning/core/services/repositories/history_repository.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/learning/provider/section_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/features/history/models/history_model.dart'; + +class HistoryProvider with ChangeNotifier { + final HistoryRepository _repository; + final SectionProvider _sectionProvider; + List _learningHistory = []; + int _selectedPageIndex = 0; + bool _isLoading = false; + String? _error; + + HistoryProvider( + this._repository, + this._sectionProvider, + ); + + List get learningHistory => _learningHistory; + int get selectedPageIndex => _selectedPageIndex; + bool get isLoading => _isLoading; + String? get error => _error; + + void setSelectedPageIndex(int index) { + _selectedPageIndex = index; + notifyListeners(); + } + + Future fetchLearningHistory(String token) async { + if (_sectionProvider.sections.isEmpty) { + _error = 'No sections available'; + notifyListeners(); + return; + } + + String sectionId = _sectionProvider.sections[_selectedPageIndex].id; + _isLoading = true; + _error = null; + notifyListeners(); + + try { + final history = await _repository.getLearningHistory(sectionId, token); + _learningHistory = history; + } catch (e) { + _error = 'Error fetching learning history: ${e.toString()}'; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + Color getColorForLevels(dynamic currentLevel, dynamic nextLevel) { + int? current = _parseLevel(currentLevel); + int? next = _parseLevel(nextLevel); + + if (current == null || next == null) { + return AppColors.blackColor; // Default color if parsing fails + } + + if (current == next) { + return AppColors.blackColor; + } else if (next > current) { + return AppColors.blueColor; + } else { + return AppColors.redColor; + } + } + + int? _parseLevel(dynamic level) { + if (level is int) { + return level; + } else if (level is String) { + if (level.toLowerCase() == 'pretest') { + return 0; // Treat Pretest as level 0 + } + return int.tryParse(level.replaceAll('Level ', '')); + } + return null; + } + + Future refreshData(String token) async { + await _sectionProvider.fetchSections(token); + await fetchLearningHistory(token); + } +} diff --git a/lib/features/history/screens/history_screen.dart b/lib/features/history/screens/history_screen.dart new file mode 100644 index 0000000..83e26b1 --- /dev/null +++ b/lib/features/history/screens/history_screen.dart @@ -0,0 +1,163 @@ +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/history/provider/history_provider.dart'; +import 'package:english_learning/features/history/widgets/custom_tab_bar.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/history/widgets/exercise_history_card.dart'; +import 'package:english_learning/features/learning/screens/learning_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; + +class HistoryScreen extends StatefulWidget { + const HistoryScreen({ + super.key, + }); + @override + State createState() => _HistoryScreenState(); +} + +class _HistoryScreenState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final historyProvider = + Provider.of(context, listen: false); + final userProvider = Provider.of(context, listen: false); + + historyProvider.refreshData(userProvider.jwtToken!); + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgSoftColor, + body: SafeArea( + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 30.0, + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Your Exercise History!', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Track your progress with a personalized overview of all your workouts.', + style: AppTextStyles.greyTextStyle.copyWith( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + const CustomTabBar(), + const SizedBox(height: 8), + Expanded( + child: Consumer( + builder: (context, historyProvider, child) { + if (historyProvider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (historyProvider.error != null) { + return Center(child: Text(historyProvider.error!)); + } else if (historyProvider.learningHistory.isEmpty) { + return _buildEmptyState(context); + } else { + return ListView.builder( + itemCount: historyProvider.learningHistory.length, + itemBuilder: (context, index) { + return Column( + children: [ + ExerciseHistoryCard( + exercise: + historyProvider.learningHistory[index], + ), + const SizedBox(height: 8.0), + ], + ); + }, + ); + } + }, + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: AppColors.whiteColor, + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Still New?', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'Begin your journey!', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 16), + SvgPicture.asset( + 'lib/features/history/assets/images/is_empty_illustration.svg', + width: 160, + ), + const SizedBox(height: 32), + GlobalButton( + text: 'Explore', + backgroundColor: AppColors.yellowButtonColor, + textColor: AppColors.blackColor, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const LearningScreen(), + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/history/widgets/custom_tab_bar.dart b/lib/features/history/widgets/custom_tab_bar.dart new file mode 100644 index 0000000..2f61a4b --- /dev/null +++ b/lib/features/history/widgets/custom_tab_bar.dart @@ -0,0 +1,101 @@ +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/history/provider/history_provider.dart'; +import 'package:english_learning/features/learning/provider/section_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:provider/provider.dart'; + +class CustomTabBar extends StatefulWidget { + const CustomTabBar({super.key}); + + @override + _CustomTabBarState createState() => _CustomTabBarState(); +} + +class _CustomTabBarState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final historyProvider = Provider.of(context); + final sectionProvider = Provider.of(context); + final selectedPageIndex = historyProvider.selectedPageIndex; + + if (sectionProvider.sections.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + return SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + child: Row( + children: sectionProvider.sections.asMap().entries.map((entry) { + int index = entry.key; + String sectionName = entry.value.name; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: _TabBarButton( + title: sectionName, + isSelected: selectedPageIndex == index, + onTap: () => _setSelectedPageIndex(context, index), + ), + ); + }).toList(), + ), + ); + } + + void _setSelectedPageIndex(BuildContext context, int index) { + final historyProvider = + Provider.of(context, listen: false); + historyProvider.setSelectedPageIndex(index); + + _scrollController.animateTo( + index * 100.0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + + final userProvider = Provider.of(context, listen: false); + historyProvider.fetchLearningHistory(userProvider.jwtToken!); + } +} + +class _TabBarButton extends StatelessWidget { + final String title; + final bool isSelected; + final VoidCallback onTap; + + const _TabBarButton({ + required this.title, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + decoration: BoxDecoration( + color: isSelected ? AppColors.blueColor : AppColors.whiteColor, + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + title, + style: TextStyle( + color: isSelected ? AppColors.whiteColor : AppColors.disableColor, + fontWeight: isSelected ? FontWeight.w900 : FontWeight.normal, + ), + ), + ), + ); + } +} diff --git a/lib/features/history/widgets/exercise_history_card.dart b/lib/features/history/widgets/exercise_history_card.dart new file mode 100644 index 0000000..bb7c1af --- /dev/null +++ b/lib/features/history/widgets/exercise_history_card.dart @@ -0,0 +1,114 @@ +import 'package:english_learning/features/history/models/history_model.dart'; +import 'package:english_learning/features/history/provider/history_provider.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ExerciseHistoryCard extends StatelessWidget { + final LearningHistory exercise; + + const ExerciseHistoryCard({ + super.key, + required this.exercise, + }); + + @override + Widget build(BuildContext context) { + final historyProvider = + Provider.of(context, listen: false); + final color = historyProvider.getColorForLevels( + exercise.currentLevel, exercise.nextLevel); + + return Card( + color: AppColors.whiteColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + elevation: 0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 18.0), + child: Row( + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + children: [ + TextSpan( + text: '${exercise.topicName} /', + ), + TextSpan( + text: ' ${exercise.sectionName}', + style: AppTextStyles.blackTextStyle) + ]), + ), + const SizedBox(height: 8), + RichText( + text: TextSpan( + style: AppTextStyles.greyTextStyle.copyWith( + fontSize: 12, + ), + children: [ + TextSpan( + text: '${exercise.currentLevel} → ', + style: AppTextStyles.blackTextStyle.copyWith( + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: '${exercise.nextLevel}', + style: AppTextStyles.blackTextStyle.copyWith( + color: color, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + Text( + 'Submission: ${exercise.formattedDate}', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 26.0, + ), + decoration: BoxDecoration( + border: Border.all( + color: color, + ), + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + '${exercise.score}/100', + style: TextStyle( + color: color, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/assets/images/Logo.svg b/lib/features/home/assets/images/Logo.svg new file mode 100644 index 0000000..72dbb30 --- /dev/null +++ b/lib/features/home/assets/images/Logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/features/home/assets/images/banner_1.png b/lib/features/home/assets/images/banner_1.png new file mode 100644 index 0000000..6924754 Binary files /dev/null and b/lib/features/home/assets/images/banner_1.png differ diff --git a/lib/features/home/assets/images/banner_2.png b/lib/features/home/assets/images/banner_2.png new file mode 100644 index 0000000..7509477 Binary files /dev/null and b/lib/features/home/assets/images/banner_2.png differ diff --git a/lib/features/home/assets/images/banner_3.png b/lib/features/home/assets/images/banner_3.png new file mode 100644 index 0000000..be5fe31 Binary files /dev/null and b/lib/features/home/assets/images/banner_3.png differ diff --git a/lib/features/home/assets/images/card_illustration.svg b/lib/features/home/assets/images/card_illustration.svg new file mode 100644 index 0000000..104c9cf --- /dev/null +++ b/lib/features/home/assets/images/card_illustration.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/home/data/card_data.dart b/lib/features/home/data/card_data.dart new file mode 100644 index 0000000..2dcb9f7 --- /dev/null +++ b/lib/features/home/data/card_data.dart @@ -0,0 +1,15 @@ +import 'package:english_learning/features/home/models/card_model.dart'; + +class CardData { + List cardData = [ + CardModel( + image: 'lib/features/home/assets/images/banner_1.png', + ), + CardModel( + image: 'lib/features/home/assets/images/banner_2.png', + ), + CardModel( + image: 'lib/features/home/assets/images/banner_3.png', + ), + ]; +} diff --git a/lib/features/home/models/card_model.dart b/lib/features/home/models/card_model.dart new file mode 100644 index 0000000..43a8cdc --- /dev/null +++ b/lib/features/home/models/card_model.dart @@ -0,0 +1,7 @@ +class CardModel { + final String image; + + CardModel({ + required this.image, + }); +} diff --git a/lib/features/home/provider/home_provider.dart b/lib/features/home/provider/home_provider.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/features/home/provider/home_provider.dart @@ -0,0 +1 @@ + diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart new file mode 100644 index 0000000..9cf2082 --- /dev/null +++ b/lib/features/home/screens/home_screen.dart @@ -0,0 +1,291 @@ +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/history/screens/history_screen.dart'; +import 'package:english_learning/features/home/data/card_data.dart'; +import 'package:english_learning/features/home/widgets/progress_card.dart'; +import 'package:english_learning/features/home/widgets/welcome_card.dart'; +import 'package:english_learning/features/learning/screens/learning_screen.dart'; +import 'package:english_learning/features/settings/modules/edit_profile/screens/edit_profile_screen.dart'; +import 'package:english_learning/features/settings/screens/settings_screen.dart'; +import 'package:english_learning/core/widgets/slider_widget.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:google_nav_bar/google_nav_bar.dart'; +import 'package:provider/provider.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); + + static void navigateReplacing(BuildContext context) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => const HomeScreen()), + ); + } +} + +class _HomeScreenState extends State { + final PageController _pageController = PageController(); + int _selectedIndex = 0; + + final List _screens = [ + const HomeContent(), + const LearningScreen(), + const HistoryScreen(), + const SettingsScreen(), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + color: AppColors.bgSoftColor, + child: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: _pageController, + children: _screens, + onPageChanged: (index) { + setState(() { + _selectedIndex = index; + }); + }, + ), + ), + bottomNavigationBar: Container( + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 24, + left: 16, + right: 16, + ), + child: GNav( + activeColor: AppColors.blueColor, + tabBackgroundColor: AppColors.whiteColor, + tabBorderRadius: 100, + color: AppColors.whiteColor, + iconSize: 20, + gap: 8, + selectedIndex: _selectedIndex, + onTabChange: (index) { + setState(() { + _selectedIndex = index; + _pageController.animateToPage( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, // Animasi ketika berpindah tab + ); + }); + }, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + tabs: const [ + GButton( + icon: BootstrapIcons.house, + text: 'Home', + ), + GButton( + icon: BootstrapIcons.book, + text: 'Learning', + ), + GButton( + icon: BootstrapIcons.clock_history, + text: 'History', + ), + GButton( + icon: BootstrapIcons.gear, + text: 'Settings', + ), + ], + ), + ), + ), + ); + } +} + +class HomeContent extends StatefulWidget { + const HomeContent({super.key}); + + @override + State createState() => _HomeContentState(); +} + +class _HomeContentState extends State { + final CardData cardData = CardData(); + int _currentPage = 0; + bool hasOngoingExercises = false; + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, authProvider, child) { + final userName = authProvider.getUserName() ?? 'Guest'; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), + ), + child: Padding( + padding: const EdgeInsets.only( + top: 60.0, + left: 18.34, + right: 16.0, + bottom: 34.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + 'lib/features/home/assets/images/Logo.svg', + width: 31, + ), + const SizedBox(width: 4.34), + Text( + 'SEALS', + style: AppTextStyles.logoTextStyle.copyWith( + fontSize: 28, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const EditProfileScreen(), + ), + ); + }, + child: const Icon( + BootstrapIcons.person_circle, + size: 28, + color: AppColors.whiteColor, + ), + ), + ], + ), + const SizedBox(height: 17), + RichText( + text: TextSpan( + text: 'Hi, ', + style: AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w700, + fontSize: 18, + ), + children: [ + TextSpan( + text: userName, + style: AppTextStyles.yellowTextStyle.copyWith( + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + TextSpan( + text: '!', + style: AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + Text( + 'Let\'s evolve together', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + const SizedBox(height: 16), + CarouselSlider.builder( + itemCount: cardData.cardData.length, + itemBuilder: (context, index, realIndex) { + return WelcomeCard(cardModel: cardData.cardData[index]); + }, + options: CarouselOptions( + height: 168, + viewportFraction: 0.9, + enlargeCenterPage: true, + autoPlay: true, + autoPlayInterval: const Duration(seconds: 3), + autoPlayAnimationDuration: const Duration(milliseconds: 800), + autoPlayCurve: Curves.fastOutSlowIn, + onPageChanged: (index, reason) { + setState( + () { + _currentPage = index; + }, + ); + }, + ), + ), + const SizedBox(height: 16), + SliderWidget( + currentPage: _currentPage, + itemCount: cardData.cardData.length, + ), + const SizedBox(height: 16), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: ProgressCard(), + ), + // hasOngoingExercises + // ? const Padding( + // padding: EdgeInsets.symmetric(horizontal: 16.0), + // child: + // ProgressCard(), // Display progress card if exercises are completed + // ) + // : const Padding( + // padding: EdgeInsets.symmetric(horizontal: 16.0), + // child: + // ExploreCard(), // Display ExploreCard if no exercises are completed + // ), + ], + ); + }); + } +} diff --git a/lib/features/home/screens/page_view.dart b/lib/features/home/screens/page_view.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/home/widgets/explore_card.dart b/lib/features/home/widgets/explore_card.dart new file mode 100644 index 0000000..b671c3d --- /dev/null +++ b/lib/features/home/widgets/explore_card.dart @@ -0,0 +1,83 @@ +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/features/learning/screens/learning_screen.dart'; +import 'package:flutter/material.dart'; + +class ExploreCard extends StatelessWidget { + const ExploreCard({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 16.0, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + const Icon( + BootstrapIcons.info_circle, + color: AppColors.disableColor, + size: 16, + ), + const SizedBox(width: 8), + Text( + 'Your Last Journey!', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + const SizedBox(height: 24), + Text( + 'Still new?', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 15, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 4), + Text( + 'Begin your journey!', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 35), + GlobalButton( + text: 'Explore', + textColor: AppColors.blackColor, + backgroundColor: AppColors.yellowButtonColor, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const LearningScreen(), + ), + ); + }, + ), + ], + ), + ), + ) + ], + ); + } +} diff --git a/lib/features/home/widgets/progress_bar.dart b/lib/features/home/widgets/progress_bar.dart new file mode 100644 index 0000000..b27b9bc --- /dev/null +++ b/lib/features/home/widgets/progress_bar.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; + +class ProgressBar extends StatelessWidget { + final int currentProgress; + final int totalProgress; + + const ProgressBar({ + super.key, + required this.currentProgress, + required this.totalProgress, + }); + + @override + Widget build(BuildContext context) { + final progress = totalProgress > 0 ? currentProgress / totalProgress : 0.0; + + return LayoutBuilder( + builder: (context, constraints) { + final barWidth = constraints.maxWidth - 40; + + return Row( + children: [ + SizedBox( + width: barWidth, + child: Container( + height: 12, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(7), + ), + child: Stack( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: barWidth * progress, + decoration: BoxDecoration( + color: AppColors.blueColor, + borderRadius: BorderRadius.circular(7), + ), + ), + ], + ), + ), + ), + const Spacer(), + Text( + '$currentProgress/$totalProgress', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/features/home/widgets/progress_card.dart b/lib/features/home/widgets/progress_card.dart new file mode 100644 index 0000000..98fff04 --- /dev/null +++ b/lib/features/home/widgets/progress_card.dart @@ -0,0 +1,91 @@ +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/home/widgets/progress_bar.dart'; +import 'package:flutter/material.dart'; + +class ProgressCard extends StatelessWidget { + const ProgressCard({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 16.0, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + const Icon( + BootstrapIcons.info_circle, + color: AppColors.disableColor, + size: 16, + ), + const SizedBox(width: 8), + Text( + 'Your Last Journey!', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + const SizedBox(height: 24), + Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppColors.disableColor, + ), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Listening', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 15, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 4), + Text( + 'Topic 8: Entertaining | Level 3', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + const ProgressBar( + currentProgress: 8, + totalProgress: 11, + ), + ], + ), + ), + ), + ], + ), + ), + ) + ], + ); + } +} diff --git a/lib/features/home/widgets/welcome_card.dart b/lib/features/home/widgets/welcome_card.dart new file mode 100644 index 0000000..4dcb626 --- /dev/null +++ b/lib/features/home/widgets/welcome_card.dart @@ -0,0 +1,27 @@ +import 'package:english_learning/features/home/models/card_model.dart'; +import 'package:flutter/material.dart'; + +class WelcomeCard extends StatelessWidget { + final CardModel cardModel; + + const WelcomeCard({ + required this.cardModel, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.asset( + cardModel.image, + fit: BoxFit.cover, + ), + ), + ); + } +} diff --git a/lib/features/learning/assets/images/Grammar.png b/lib/features/learning/assets/images/Grammar.png new file mode 100644 index 0000000..6add693 Binary files /dev/null and b/lib/features/learning/assets/images/Grammar.png differ diff --git a/lib/features/learning/assets/images/Listening.png b/lib/features/learning/assets/images/Listening.png new file mode 100644 index 0000000..904f33c Binary files /dev/null and b/lib/features/learning/assets/images/Listening.png differ diff --git a/lib/features/learning/assets/images/Reading.png b/lib/features/learning/assets/images/Reading.png new file mode 100644 index 0000000..58babf2 Binary files /dev/null and b/lib/features/learning/assets/images/Reading.png differ diff --git a/lib/features/learning/assets/images/Vocabulary.png b/lib/features/learning/assets/images/Vocabulary.png new file mode 100644 index 0000000..644901e Binary files /dev/null and b/lib/features/learning/assets/images/Vocabulary.png differ diff --git a/lib/features/learning/assets/images/drive-download-20240911T015135Z-001.zip b/lib/features/learning/assets/images/drive-download-20240911T015135Z-001.zip new file mode 100644 index 0000000..51cd808 Binary files /dev/null and b/lib/features/learning/assets/images/drive-download-20240911T015135Z-001.zip differ diff --git a/lib/features/learning/assets/images/test.png b/lib/features/learning/assets/images/test.png new file mode 100644 index 0000000..cd00b13 Binary files /dev/null and b/lib/features/learning/assets/images/test.png differ diff --git a/lib/features/learning/modules/exercises/assets/images/complete_illustration.svg b/lib/features/learning/modules/exercises/assets/images/complete_illustration.svg new file mode 100644 index 0000000..bd0cfdb --- /dev/null +++ b/lib/features/learning/modules/exercises/assets/images/complete_illustration.svg @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/learning/modules/exercises/assets/images/incomplete_illustration.svg b/lib/features/learning/modules/exercises/assets/images/incomplete_illustration.svg new file mode 100644 index 0000000..0a23a22 --- /dev/null +++ b/lib/features/learning/modules/exercises/assets/images/incomplete_illustration.svg @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/learning/modules/exercises/assets/images/material_illustration.png b/lib/features/learning/modules/exercises/assets/images/material_illustration.png new file mode 100644 index 0000000..295429b Binary files /dev/null and b/lib/features/learning/modules/exercises/assets/images/material_illustration.png differ diff --git a/lib/features/learning/modules/exercises/assets/images/paris.jpg b/lib/features/learning/modules/exercises/assets/images/paris.jpg new file mode 100644 index 0000000..86d03a4 Binary files /dev/null and b/lib/features/learning/modules/exercises/assets/images/paris.jpg differ diff --git a/lib/features/learning/modules/exercises/models/exercise_model.dart b/lib/features/learning/modules/exercises/models/exercise_model.dart new file mode 100644 index 0000000..0f1ead9 --- /dev/null +++ b/lib/features/learning/modules/exercises/models/exercise_model.dart @@ -0,0 +1,144 @@ +class ExerciseModel { + final String idAdminExercise; + final String idLevel; + final String title; + final String question; + final String questionType; + final String? audio; + final String? video; + final String? image; + final DateTime timeAdminExc; + final dynamic choices; + + ExerciseModel({ + required this.idAdminExercise, + required this.idLevel, + required this.title, + required this.question, + required this.questionType, + this.audio, + this.video, + this.image, + required this.timeAdminExc, + required this.choices, + }); + + factory ExerciseModel.fromJson(Map json) { + dynamic choices; + switch (json['QUESTION_TYPE']) { + case 'MCQ': + if (json['multipleChoices'] != null && + json['multipleChoices'].isNotEmpty) { + choices = MultipleChoice.fromJson(json['multipleChoices'][0]); + } + break; + case 'TFQ': + if (json['trueFalse'] != null && json['trueFalse'].isNotEmpty) { + choices = TrueFalse.fromJson(json['trueFalse'][0]); + } + break; + case 'MPQ': + if (json['matchingPairs'] != null && json['matchingPairs'].isNotEmpty) { + choices = MatchingPair.fromJsonList(json['matchingPairs']); + } + break; + } + + return ExerciseModel( + idAdminExercise: json['ID_ADMIN_EXERCISE'], + idLevel: json['ID_LEVEL'], + title: json['TITLE'], + question: json['QUESTION'], + questionType: json['QUESTION_TYPE'], + audio: json['AUDIO'], + video: json['VIDEO'], + image: json['IMAGE'], + timeAdminExc: DateTime.parse(json['TIME_ADMIN_EXC']), + choices: choices, + ); + } +} + +class MultipleChoice { + final String idMultipleChoices; + final String idAdminExercise; + final String optionA; + final String optionB; + final String optionC; + final String optionD; + final String optionE; + final DateTime timeMultipleChoices; + + MultipleChoice({ + required this.idMultipleChoices, + required this.idAdminExercise, + required this.optionA, + required this.optionB, + required this.optionC, + required this.optionD, + required this.optionE, + required this.timeMultipleChoices, + }); + + factory MultipleChoice.fromJson(Map json) { + return MultipleChoice( + idMultipleChoices: json['ID_MULTIPLE_CHOICES'], + idAdminExercise: json['ID_ADMIN_EXERCISE'], + optionA: json['OPTION_A'], + optionB: json['OPTION_B'], + optionC: json['OPTION_C'], + optionD: json['OPTION_D'], + optionE: json['OPTION_E'], + timeMultipleChoices: DateTime.parse(json['TIME_MULTIPLE_CHOICES']), + ); + } +} + +class TrueFalse { + final String idTrueFalse; + final String idAdminExercise; + final DateTime timeTrueFalse; + + TrueFalse({ + required this.idTrueFalse, + required this.idAdminExercise, + required this.timeTrueFalse, + }); + + factory TrueFalse.fromJson(Map json) { + return TrueFalse( + idTrueFalse: json['ID_TRUE_FALSE'], + idAdminExercise: json['ID_ADMIN_EXERCISE'], + timeTrueFalse: DateTime.parse(json['TIME_TRUE_FALSE']), + ); + } +} + +class Pair { + final String left; + final String right; + + Pair({required this.left, required this.right}); +} + +class MatchingPair { + final List pairs; + + MatchingPair({required this.pairs}); + + factory MatchingPair.fromJsonList(List? jsonList) { + if (jsonList == null) { + return MatchingPair( + pairs: []); // Return an empty list if jsonList is null + } + + List pairs = []; + for (var pair in jsonList) { + pairs.add(Pair( + left: pair['LEFT_PAIR'] ?? '', // Use empty string as default if null + right: pair['RIGHT_PAIR'] ?? '', + )); + } + return MatchingPair(pairs: pairs); + } +} diff --git a/lib/features/learning/modules/exercises/providers/exercise_provider.dart b/lib/features/learning/modules/exercises/providers/exercise_provider.dart new file mode 100644 index 0000000..4a08c7f --- /dev/null +++ b/lib/features/learning/modules/exercises/providers/exercise_provider.dart @@ -0,0 +1,317 @@ +import 'package:flutter/material.dart'; +import 'package:english_learning/core/services/repositories/exercise_repository.dart'; +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/learning/modules/exercises/models/exercise_model.dart'; + +class ExerciseProvider extends ChangeNotifier { + // Dependencies + final ExerciseRepository _repository; + final UserProvider _userProvider; + + // Constructor + ExerciseProvider(this._repository, this._userProvider); + + // State variables + List _exercises = []; + Map>> _matchingAnswers = {}; + List _answers = []; + List> _leftColors = []; + List> _rightColors = []; + int _currentExerciseIndex = 0; + bool _isLoading = false; + String _nameTopic = ''; + String _nameLevel = ''; + String? _activeLeftOption; + String? _studentLearningId; + + // Constants + final List _pairColors = [ + Colors.blue, + Colors.green, + Colors.orange, + Colors.purple, + Colors.red, + ]; + + // Getters + List get exercises => _exercises; + List get answers => _answers; + int get currentExerciseIndex => _currentExerciseIndex; + bool get isLoading => _isLoading; + String get nameTopic => _nameTopic; + String get nameLevel => _nameLevel; + String? get activeLeftOption => _activeLeftOption; + String? get studentLearningId => _studentLearningId; + + // Initialization methods + void initializeAnswers() { + _answers = List.generate(_exercises.length, (index) => ''); + _matchingAnswers = {}; + _leftColors = List.generate(_exercises.length, (index) { + if (_exercises[index].choices is MatchingPair) { + return List.generate( + (_exercises[index].choices as MatchingPair).pairs.length, + (i) => _pairColors[i % _pairColors.length]); + } + return []; + }); + _rightColors = List.generate(_exercises.length, (index) { + if (_exercises[index].choices is MatchingPair) { + return List.filled( + (_exercises[index].choices as MatchingPair).pairs.length, null); + } + return []; + }); + } + + // Answer handling methods + void answerQuestion(int index, String answer) { + if (index >= 0 && index < _answers.length) { + if (_exercises[index].choices is MatchingPair) { + _handleMatchingPairAnswer(index, answer); + } else { + _answers[index] = _answers[index] == answer ? '' : answer; + } + notifyListeners(); + } + } + + void _handleMatchingPairAnswer(int exerciseIndex, String option) { + final matchingPair = _exercises[exerciseIndex].choices as MatchingPair; + final isLeft = matchingPair.pairs.any((pair) => pair.left == option); + + if (isLeft) { + _selectLeftOption(exerciseIndex, option); + } else { + _selectRightOption(exerciseIndex, option); + } + + _updateMatchingPairAnswer(exerciseIndex); + notifyListeners(); + } + + void _selectLeftOption(int exerciseIndex, String option) { + if (_activeLeftOption == option) { + _activeLeftOption = null; + } else { + _activeLeftOption = option; + } + } + + void _selectRightOption(int exerciseIndex, String option) { + if (_activeLeftOption == null) { + print("Please select a left option first."); + return; + } + + final pairs = (_exercises[exerciseIndex].choices as MatchingPair).pairs; + final leftIndex = + pairs.indexWhere((pair) => pair.left == _activeLeftOption); + final rightIndex = pairs.indexWhere((pair) => pair.right == option); + + if (leftIndex != -1 && rightIndex != -1) { + if (!_matchingAnswers.containsKey(exerciseIndex)) { + _matchingAnswers[exerciseIndex] = []; + } + + _matchingAnswers[exerciseIndex]!.removeWhere((pair) => + pair['left'] == _activeLeftOption || pair['right'] == option); + + _matchingAnswers[exerciseIndex]! + .add({'left': _activeLeftOption!, 'right': option}); + + _rightColors[exerciseIndex][rightIndex] = + _leftColors[exerciseIndex][leftIndex]; + + _activeLeftOption = null; + } + } + + void _updateMatchingPairAnswer(int exerciseIndex) { + if (_matchingAnswers[exerciseIndex] != null && + _matchingAnswers[exerciseIndex]!.length == + (_exercises[exerciseIndex].choices as MatchingPair).pairs.length) { + _answers[exerciseIndex] = _matchingAnswers[exerciseIndex]! + .map((pair) => "${pair['left']}-${pair['right']}") + .join(", "); + } else { + _answers[exerciseIndex] = ''; + } + } + + // Utility methods + bool isAllPairsMatched(int exerciseIndex) { + if (_exercises[exerciseIndex].choices is! MatchingPair) return false; + final matchingPair = _exercises[exerciseIndex].choices as MatchingPair; + return _matchingAnswers[exerciseIndex] != null && + _matchingAnswers[exerciseIndex]!.length == matchingPair.pairs.length && + _matchingAnswers[exerciseIndex]!.every( + (pair) => pair['left']!.isNotEmpty && pair['right']!.isNotEmpty); + } + + String? getMatchingAnswer(int exerciseIndex, int pairIndex, bool isLeft) { + if (_matchingAnswers.containsKey(exerciseIndex) && + pairIndex < _matchingAnswers[exerciseIndex]!.length) { + return _matchingAnswers[exerciseIndex]![pairIndex] + [isLeft ? 'left' : 'right']; + } + return null; + } + + Color? getLeftColor(int exerciseIndex, int pairIndex) { + return _leftColors[exerciseIndex][pairIndex]; + } + + Color? getRightColor(int exerciseIndex, int pairIndex) { + return _rightColors[exerciseIndex][pairIndex]; + } + + bool isOptionSelected(int exerciseIndex, String option, bool isLeft) { + if (_matchingAnswers[exerciseIndex] == null) return false; + return _matchingAnswers[exerciseIndex]!.any( + (pair) => isLeft ? pair['left'] == option : pair['right'] == option); + } + + // Navigation methods + void goToExercise(int index) { + if (index >= 0 && index < _exercises.length) { + _currentExerciseIndex = index; + notifyListeners(); + } + } + + void nextQuestion() { + if (_currentExerciseIndex < _answers.length - 1) { + _currentExerciseIndex++; + notifyListeners(); + } + } + + void previousQuestion() { + if (_currentExerciseIndex > 0) { + _currentExerciseIndex--; + notifyListeners(); + } + } + + // Progress tracking methods + int getAnsweredCount() { + return _answers.where((answer) => answer.isNotEmpty).length; + } + + double getProgress() { + return getAnsweredCount() / _exercises.length; + } + + bool isExerciseCompleted(int index) { + if (_exercises[index].choices is MatchingPair) { + return isAllPairsMatched(index); + } + return _answers[index].isNotEmpty; + } + + // API methods + Future fetchExercises(String levelId) async { + _isLoading = true; + notifyListeners(); + + try { + final token = await _userProvider.getValidToken(); + if (token == null) { + throw Exception('No valid token found'); + } + + final data = await _repository.getExercises(levelId, token); + _nameTopic = data['NAME_TOPIC']; + _nameLevel = data['NAME_LEVEL']; + final exercisesData = data['EXERCISES']; + _exercises = exercisesData + .map((json) => ExerciseModel.fromJson(json)) + .toList(); + _answers = List.generate(_exercises.length, (index) => ''); + initializeAnswers(); + } catch (e) { + print('Error fetching exercises: $e'); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + Future> submitAnswersAndGetScore() async { + print('submitAnswersAndGetScore called'); + try { + if (_studentLearningId == null) { + throw Exception('Student Learning ID is not set'); + } + + final token = await _userProvider.getValidToken(); + if (token == null) { + throw Exception('No valid token found'); + } + + final answersToSubmit = _exercises.asMap().entries.where((entry) { + final index = entry.key; + return _answers[index].isNotEmpty; + }).map((entry) { + final index = entry.key; + final exercise = entry.value; + return { + 'ID_STUDENT_LEARNING': _studentLearningId!, + 'ID_ADMIN_EXERCISE': exercise.idAdminExercise, + 'ANSWER_STUDENT': _answers[index], + }; + }).toList(); + + if (answersToSubmit.isEmpty) { + throw Exception( + 'No answers to submit. Please answer at least one question.'); + } + + print('Submitting answers to repository'); + final result = await _repository.submitAnswersAndGetScore( + answersToSubmit, _studentLearningId!, token); + + print('Repository response: $result'); + + return { + 'CURRENT_LEVEL_NAME': result['CURRENT_LEVEL_NAME'], + 'NEXT_LEARNING_NAME': result['NEXT_LEARNING_NAME'], + 'SCORE': result['SCORE'], + 'IS_PASS': result['IS_PASS'], + }; + } catch (e) { + print('Error in submitAnswersAndGetScore: $e'); + rethrow; + } + } + + bool hasAnsweredQuestions() { + return _answers.any((answer) => answer.isNotEmpty); + } + + bool hasUnansweredQuestions() { + return _answers.any((answer) => answer.isEmpty); + } + + // Other methods + void setStudentLearningId(String id) { + _studentLearningId = id; + notifyListeners(); + } + + void updateFrom(ExerciseProvider? previous) { + if (previous != null) { + _exercises = previous._exercises; + _matchingAnswers = previous._matchingAnswers; + _answers = previous._answers; + _leftColors = previous._leftColors; + _rightColors = previous._rightColors; + _currentExerciseIndex = previous._currentExerciseIndex; + _isLoading = previous._isLoading; + _nameTopic = previous._nameTopic; + _nameLevel = previous._nameLevel; + _studentLearningId = previous._studentLearningId; + } + } +} diff --git a/lib/features/learning/modules/exercises/screens/exercise_screen.dart b/lib/features/learning/modules/exercises/screens/exercise_screen.dart new file mode 100644 index 0000000..bd79e0c --- /dev/null +++ b/lib/features/learning/modules/exercises/screens/exercise_screen.dart @@ -0,0 +1,124 @@ +import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; +import 'package:english_learning/features/learning/modules/exercises/widgets/exercise_content.dart'; +import 'package:english_learning/features/learning/modules/exercises/widgets/exercise_navigator.dart'; +import 'package:english_learning/features/learning/modules/exercises/widgets/exercise_progress.dart'; +import 'package:english_learning/features/learning/modules/exercises/widgets/instruction_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:provider/provider.dart'; + +class ExerciseScreen extends StatefulWidget { + final String? levelId; + final String studentLearningId; + + const ExerciseScreen({ + super.key, + required this.levelId, + required this.studentLearningId, + }); + + @override + State createState() => _ExerciseScreenState(); +} + +class _ExerciseScreenState extends State { + late ScrollController _scrollController; + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final provider = context.read(); + provider.fetchExercises(widget.levelId!); + provider.setStudentLearningId(widget.studentLearningId); + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _scrollToTop() { + _scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + if (provider.isLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + return Scaffold( + backgroundColor: AppColors.bgSoftColor, + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + iconTheme: const IconThemeData(color: AppColors.whiteColor), + centerTitle: true, + title: Text( + '${provider.nameTopic} - ${provider.nameLevel}', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w900, + ), + ), + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + ), + ), + actions: [ + IconButton( + icon: const Icon(Icons.info_outline, color: AppColors.whiteColor), + onPressed: () => _showInstructions(context), + ), + ], + ), + body: SafeArea( + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(16.0), + child: ExerciseProgress(), + ), + Expanded( + child: RefreshIndicator( + onRefresh: () => provider.fetchExercises(widget.levelId!), + child: SingleChildScrollView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + const SizedBox(height: 16), + const ExerciseContent(), + const SizedBox(height: 24), + ExerciseNavigator(onScrollToTop: _scrollToTop), + const SizedBox(height: 32), + ], + ), + ), + ), + ), + ], + ), + ), + ); + }); + } + + void _showInstructions(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) => const InstructionsDialog(), + ); + } +} diff --git a/lib/features/learning/modules/exercises/widgets/complete_submission.dart b/lib/features/learning/modules/exercises/widgets/complete_submission.dart new file mode 100644 index 0000000..3fdeec3 --- /dev/null +++ b/lib/features/learning/modules/exercises/widgets/complete_submission.dart @@ -0,0 +1,101 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class CompleteSubmission extends StatelessWidget { + final VoidCallback onCheckAgain; + final VoidCallback onSubmit; + + const CompleteSubmission({ + super.key, + required this.onSubmit, + required this.onCheckAgain, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 24.0, + horizontal: 18.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + text: TextSpan(children: [ + TextSpan( + text: 'Proceed With', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + TextSpan( + text: ' Submission ', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + TextSpan( + text: '?', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + ]), + ), + const SizedBox(height: 20), + SvgPicture.asset( + 'lib/features/learning/modules/exercises/assets/images/complete_illustration.svg', + width: 160, + ), + const SizedBox(height: 20), + Text( + 'Confirm submission? There\'s no going back.', + textAlign: TextAlign.center, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: GlobalButton( + text: 'Check Again', + textColor: AppColors.blueColor, + borderColor: AppColors.blueColor, + backgroundColor: Colors.transparent, + onPressed: onCheckAgain, + ), + ), + const SizedBox(width: 10), + Expanded( + child: GlobalButton( + text: 'Confirm', + onPressed: onSubmit, + ), + ) + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/learning/modules/exercises/widgets/exercise_content.dart b/lib/features/learning/modules/exercises/widgets/exercise_content.dart new file mode 100644 index 0000000..627b849 --- /dev/null +++ b/lib/features/learning/modules/exercises/widgets/exercise_content.dart @@ -0,0 +1,149 @@ +import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/learning/modules/exercises/models/exercise_model.dart'; +import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; +import 'package:english_learning/features/learning/modules/exercises/widgets/question/matching_pairs_question.dart'; +import 'package:english_learning/features/learning/modules/exercises/widgets/question/multiple_choice_question.dart'; +import 'package:english_learning/features/learning/modules/exercises/widgets/question/true_false_question.dart'; +import 'package:english_learning/features/learning/modules/exercises/widgets/see_progress_modal.dart'; +import 'package:english_learning/features/learning/modules/material/widgets/image_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ExerciseContent extends StatelessWidget { + const ExerciseContent({super.key}); + + Widget _buildQuestionWidget( + ExerciseModel exercise, + ExerciseProvider provider, + ) { + switch (exercise.questionType) { + case 'MCQ': + return MultipleChoiceQuestion( + exercise: exercise, + ); + case 'TFQ': + return TrueFalseQuestion( + exercise: exercise, + ); + case 'MPQ': + return MatchingPairsQuestion( + exercise: exercise, + ); + default: + return const Text('Unsupported question type'); + } + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + if (provider.exercises.isEmpty) { + return const Center(child: Text('No exercises available')); + } + final exercise = provider.exercises[provider.currentExerciseIndex]; + + return TweenAnimationBuilder( + duration: const Duration(milliseconds: 500), + tween: Tween(begin: 0, end: 1), + builder: (context, double value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(50 * (1 - value), 0), + child: child, + ), + ); + }, + child: Container( + key: ValueKey(provider.currentExerciseIndex), + width: double.infinity, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Question ${provider.currentExerciseIndex + 1}', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + GestureDetector( + onTap: () { + showModalBottomSheet( + backgroundColor: AppColors.whiteColor, + context: context, + builder: (context) => const SeeProgressModal(), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + ); + }, + child: Text( + 'See Progress', + style: AppTextStyles.blueTextStyle.copyWith( + decoration: TextDecoration.underline, + decorationColor: AppColors.blueColor, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + const Divider( + color: AppColors.disableColor, + thickness: 1, + ), + const SizedBox(height: 12), + Text( + exercise.question, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 16), + if (exercise.image != null && exercise.image!.isNotEmpty) + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: ImageWidget( + imageFileName: exercise.image!, + baseUrl: '${baseUrl}uploads/exercise/image/', + ), + ), + ), + const SizedBox(height: 14), + _buildQuestionWidget( + exercise, + provider, + ) + ], + ), + ), + ), + ); + }); + } +} diff --git a/lib/features/learning/modules/exercises/widgets/exercise_navigator.dart b/lib/features/learning/modules/exercises/widgets/exercise_navigator.dart new file mode 100644 index 0000000..da312f7 --- /dev/null +++ b/lib/features/learning/modules/exercises/widgets/exercise_navigator.dart @@ -0,0 +1,191 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/learning/modules/exercises/widgets/complete_submission.dart'; +import 'package:english_learning/features/learning/modules/exercises/widgets/incomplete_submission.dart'; +import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; +import 'package:english_learning/features/learning/modules/result/screens/result_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ExerciseNavigator extends StatelessWidget { + final VoidCallback? onScrollToTop; + const ExerciseNavigator({ + super.key, + required this.onScrollToTop, + }); + + @override + Widget build(BuildContext context) { + return Consumer2( + builder: (context, exerciseProvider, userProvider, _) { + final currentExerciseIndex = exerciseProvider.currentExerciseIndex; + final isFirstExercise = currentExerciseIndex == 0; + final isLastExercise = + currentExerciseIndex == exerciseProvider.exercises.length - 1; + + Future submitAnswers() async { + try { + final result = await exerciseProvider.submitAnswersAndGetScore(); + print('Submit result: $result'); // Logging + + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => ResultScreen( + currentLevel: result['CURRENT_LEVEL_NAME'], + nextLevel: result['NEXT_LEARNING_NAME'], + score: int.parse(result['SCORE'].toString()), + isCompleted: result['IS_PASS'] == 1, + ), + ), + ); + } catch (e) { + print('Error submitting answers: $e'); // Logging + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } + } + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!isFirstExercise) + Expanded( + child: ElevatedButton( + onPressed: () { + exerciseProvider.previousQuestion(); + onScrollToTop?.call(); + }, + style: ElevatedButton.styleFrom( + shadowColor: Colors.transparent, + backgroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide( + color: AppColors.blueColor, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Text( + 'Previous', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + if (!isFirstExercise && !isLastExercise) const SizedBox(width: 8), + if (!isLastExercise) + Expanded( + child: ElevatedButton( + onPressed: () { + exerciseProvider.nextQuestion(); + onScrollToTop?.call(); + }, + style: ElevatedButton.styleFrom( + shadowColor: Colors.transparent, + backgroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide( + color: AppColors.blueColor, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Text( + 'Next', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + if (isLastExercise && !isFirstExercise) const SizedBox(width: 8), + if (isLastExercise) + Expanded( + child: ElevatedButton( + onPressed: () { + if (exerciseProvider.hasAnsweredQuestions()) { + if (exerciseProvider.hasUnansweredQuestions()) { + // Ada pertanyaan yang belum dijawab + showDialog( + context: context, + builder: (BuildContext context) { + return IncompleteSubmission( + onCheckAgain: () { + Navigator.of(context).pop(); + }, + onSubmit: () async { + Navigator.of(context).pop(); + submitAnswers(); + }, + ); + }, + ); + } else { + // Semua pertanyaan sudah dijawab + showDialog( + context: context, + builder: (BuildContext context) { + return CompleteSubmission( + onCheckAgain: () { + Navigator.of(context).pop(); + }, + onSubmit: () async { + Navigator.of(context).pop(); + submitAnswers(); + }, + ); + }, + ); + } + } else { + // Tidak ada pertanyaan yang dijawab + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Please answer at least one question before submitting.')), + ); + } + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Ink( + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + borderRadius: BorderRadius.circular(12), + ), + child: GestureDetector( + onTap: exerciseProvider.nextQuestion, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + alignment: Alignment.center, + child: Text( + 'Submit', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ), + ], + ); + }); + } +} diff --git a/lib/features/learning/modules/exercises/widgets/exercise_progress.dart b/lib/features/learning/modules/exercises/widgets/exercise_progress.dart new file mode 100644 index 0000000..538dcfa --- /dev/null +++ b/lib/features/learning/modules/exercises/widgets/exercise_progress.dart @@ -0,0 +1,67 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ExerciseProgress extends StatelessWidget { + const ExerciseProgress({super.key}); + + @override + Widget build(BuildContext context) { + final exerciseProvider = Provider.of(context); + final totalExercises = exerciseProvider.exercises.length; + final answeredCount = exerciseProvider.getAnsweredCount(); + final progress = exerciseProvider.getProgress(); + + return LayoutBuilder( + builder: (context, constraints) { + final barWidth = + (constraints.maxWidth - 130).clamp(0.0, double.infinity); + + return Row( + children: [ + SizedBox( + width: barWidth, + child: Container( + height: 20, + decoration: BoxDecoration( + border: Border.all( + color: AppColors.blueColor, + ), + borderRadius: BorderRadius.circular(20), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Stack( + children: [ + Container( + color: AppColors.whiteColor, + ), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: (barWidth * progress).clamp(0.0, barWidth), + decoration: BoxDecoration( + color: AppColors.blueColor, + borderRadius: BorderRadius.circular(20), + ), + ), + ], + ), + ), + ), + ), + const Spacer(), + Text( + '$answeredCount of $totalExercises Answered', + style: AppTextStyles.greyTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ) + ], + ); + }, + ); + } +} diff --git a/lib/features/learning/modules/exercises/widgets/incomplete_submission.dart b/lib/features/learning/modules/exercises/widgets/incomplete_submission.dart new file mode 100644 index 0000000..3d10c66 --- /dev/null +++ b/lib/features/learning/modules/exercises/widgets/incomplete_submission.dart @@ -0,0 +1,97 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class IncompleteSubmission extends StatelessWidget { + final VoidCallback onCheckAgain; + final VoidCallback onSubmit; + + const IncompleteSubmission({ + super.key, + required this.onCheckAgain, + required this.onSubmit, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 18.0, + vertical: 24.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + text: TextSpan(children: [ + TextSpan( + text: 'Incomplete', + style: AppTextStyles.redTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + TextSpan( + text: ' Submission!', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + ]), + ), + const SizedBox(height: 20), + SvgPicture.asset( + 'lib/features/learning/modules/exercises/assets/images/incomplete_illustration.svg', + width: 160, + ), + const SizedBox(height: 20), + Text( + 'Looks like a few questions might have been missed. Would you like to review them?', + textAlign: TextAlign.center, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: GlobalButton( + text: 'Check Again', + textColor: AppColors.blueColor, + borderColor: AppColors.blueColor, + backgroundColor: Colors.transparent, + onPressed: () { + onCheckAgain(); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: GlobalButton( + text: 'Submit', + backgroundColor: AppColors.redColor, + onPressed: onSubmit, + ), + ) + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/learning/modules/exercises/widgets/instruction_dialog.dart b/lib/features/learning/modules/exercises/widgets/instruction_dialog.dart new file mode 100644 index 0000000..6f723eb --- /dev/null +++ b/lib/features/learning/modules/exercises/widgets/instruction_dialog.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; + +class InstructionsDialog extends StatelessWidget { + const InstructionsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.whiteColor, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 10.0, + offset: Offset(0.0, 10.0), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Instructions', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + _buildInstructionItem( + icon: Icons.question_answer, + text: 'Answer all questions to complete the exercise.', + ), + const SizedBox(height: 12), + _buildInstructionItem( + icon: Icons.compare_arrows, + text: + 'For matching pairs, select a left option first, then match it with a right option.', + ), + const SizedBox(height: 12), + _buildInstructionItem( + icon: Icons.edit, + text: + 'You can change your answers at any time before submitting.', + ), + const SizedBox(height: 24), + ElevatedButton( + child: const Text('Got it!'), + onPressed: () => Navigator.of(context).pop(), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.blueColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + padding: + const EdgeInsets.symmetric(horizontal: 30, vertical: 10), + ), + ), + ], + ), + ), + ); + } + + Widget _buildInstructionItem({required IconData icon, required String text}) { + return Row( + children: [ + Icon(icon, color: AppColors.blueColor), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: AppTextStyles.blackTextStyle.copyWith(fontSize: 16), + ), + ), + ], + ); + } +} diff --git a/lib/features/learning/modules/exercises/widgets/question/matching_pairs_question.dart b/lib/features/learning/modules/exercises/widgets/question/matching_pairs_question.dart new file mode 100644 index 0000000..f8fcc90 --- /dev/null +++ b/lib/features/learning/modules/exercises/widgets/question/matching_pairs_question.dart @@ -0,0 +1,124 @@ +import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/learning/modules/exercises/models/exercise_model.dart'; +import 'package:provider/provider.dart'; + +class MatchingPairsQuestion extends StatelessWidget { + final ExerciseModel exercise; + + const MatchingPairsQuestion({ + super.key, + required this.exercise, + }); + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); + final matchingPair = exercise.choices as MatchingPair; + final currentIndex = provider.currentExerciseIndex; + + return Column( + children: [ + ...matchingPair.pairs.asMap().entries.map((entry) { + int pairIndex = entry.key; + Pair pair = entry.value; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: _buildOptionItem( + context, + pair.left, + provider, + currentIndex, + pairIndex, + true, + ), + ), + const SizedBox(width: 10), + Expanded( + child: _buildOptionItem( + context, + pair.right, + provider, + currentIndex, + pairIndex, + false, + ), + ), + ], + ); + }).toList(), + ], + ); + } + + Widget _buildOptionItem( + BuildContext context, + String option, + ExerciseProvider provider, + int exerciseIndex, + int pairIndex, + bool isLeft, + ) { + Color? color = isLeft + ? provider.getLeftColor(exerciseIndex, pairIndex) + : provider.getRightColor(exerciseIndex, pairIndex); + final isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft); + final isActive = isLeft && provider.activeLeftOption == option; + + return GestureDetector( + onTap: () { + if (!isLeft && provider.activeLeftOption == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please select a left option first.')), + ); + } else { + provider.answerQuestion(exerciseIndex, option); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Container( + decoration: BoxDecoration( + color: + isSelected ? color : (isActive ? color! : AppColors.whiteColor), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(20), + bottomRight: Radius.circular(20), + bottomLeft: Radius.circular(20), + ), + border: Border.all( + color: isSelected + ? color ?? AppColors.cardDisabledColor + : AppColors.cardDisabledColor, + width: isSelected ? 2 : 1, + ), + boxShadow: isActive + ? [ + BoxShadow( + color: color?.withOpacity(0.5) ?? + Colors.grey.withOpacity(0.5), + spreadRadius: 1, + blurRadius: 4, + offset: const Offset(0, 2), + ) + ] + : null, + ), + padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0), + child: Text( + option, + style: AppTextStyles.blackTextStyle.copyWith( + fontWeight: FontWeight.w900, + color: isSelected || isActive + ? AppColors.whiteColor + : AppColors.blackColor, + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/learning/modules/exercises/widgets/question/multiple_choice_question.dart b/lib/features/learning/modules/exercises/widgets/question/multiple_choice_question.dart new file mode 100644 index 0000000..0dae68d --- /dev/null +++ b/lib/features/learning/modules/exercises/widgets/question/multiple_choice_question.dart @@ -0,0 +1,119 @@ +// multiple_choice_question.dart +import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/learning/modules/exercises/models/exercise_model.dart'; +import 'package:provider/provider.dart'; + +class MultipleChoiceQuestion extends StatelessWidget { + final ExerciseModel exercise; + + const MultipleChoiceQuestion({ + super.key, + required this.exercise, + }); + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); + final multipleChoice = exercise.choices as MultipleChoice; + final options = [ + multipleChoice.optionA, + multipleChoice.optionB, + multipleChoice.optionC, + multipleChoice.optionD, + multipleChoice.optionE, + ]; + + return _buildOptionsList(options, provider); + } + + Widget _buildOptionsList(List options, ExerciseProvider provider) { + final optionLabels = List.generate( + options.length, + (index) => String.fromCharCode(65 + index), + ); + + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: options.length, + itemBuilder: (context, i) { + final option = options[i]; + final isSelected = + provider.answers[provider.currentExerciseIndex] == option; + + return GestureDetector( + onTap: () => + provider.answerQuestion(provider.currentExerciseIndex, option), + child: _buildOptionItem(optionLabels[i], option, isSelected), + ); + }, + ); + } + + Widget _buildOptionItem(String label, String option, bool isSelected) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: isSelected ? AppColors.blueColor : AppColors.whiteColor, + borderRadius: BorderRadius.circular(25), + border: Border.all( + color: isSelected + ? Colors.transparent + : AppColors.cardDisabledColor, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14.0, + vertical: 10.0, + ), + child: Text( + label, + style: AppTextStyles.blackTextStyle.copyWith( + fontWeight: FontWeight.w900, + color: + isSelected ? AppColors.whiteColor : AppColors.blackColor, + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Container( + margin: const EdgeInsets.only(bottom: 8.0), + width: double.infinity, + decoration: BoxDecoration( + color: isSelected ? AppColors.blueColor : AppColors.whiteColor, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(20), + bottomRight: Radius.circular(20), + bottomLeft: Radius.circular(20), + ), + border: Border.all( + color: isSelected + ? Colors.transparent + : AppColors.cardDisabledColor, + ), + ), + padding: + const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), + child: Text( + option, + style: AppTextStyles.blackTextStyle.copyWith( + color: + isSelected ? AppColors.whiteColor : AppColors.blackColor, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/learning/modules/exercises/widgets/question/true_false_question.dart b/lib/features/learning/modules/exercises/widgets/question/true_false_question.dart new file mode 100644 index 0000000..62451c2 --- /dev/null +++ b/lib/features/learning/modules/exercises/widgets/question/true_false_question.dart @@ -0,0 +1,107 @@ +// true_false_question.dart +import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/learning/modules/exercises/models/exercise_model.dart'; +import 'package:provider/provider.dart'; + +class TrueFalseQuestion extends StatelessWidget { + final ExerciseModel exercise; + + const TrueFalseQuestion({ + super.key, + required this.exercise, + }); + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); + final options = ['True', 'False']; + + return _buildOptionsList(options, provider); + } + + Widget _buildOptionsList(List options, ExerciseProvider provider) { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: options.length, + itemBuilder: (context, i) { + final option = options[i]; + final isSelected = + provider.answers[provider.currentExerciseIndex] == option; + + return GestureDetector( + onTap: () => + provider.answerQuestion(provider.currentExerciseIndex, option), + child: _buildOptionItem(option, isSelected), + ); + }, + ); + } + + Widget _buildOptionItem(String option, bool isSelected) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: isSelected ? AppColors.blueColor : AppColors.whiteColor, + borderRadius: BorderRadius.circular(25), + border: Border.all( + color: isSelected + ? Colors.transparent + : AppColors.cardDisabledColor, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14.0, + vertical: 10.0, + ), + child: Text( + option, + style: AppTextStyles.blackTextStyle.copyWith( + fontWeight: FontWeight.w900, + color: + isSelected ? AppColors.whiteColor : AppColors.blackColor, + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Container( + margin: const EdgeInsets.only(bottom: 8.0), + width: double.infinity, + decoration: BoxDecoration( + color: isSelected ? AppColors.blueColor : AppColors.whiteColor, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(20), + bottomRight: Radius.circular(20), + bottomLeft: Radius.circular(20), + ), + border: Border.all( + color: isSelected + ? Colors.transparent + : AppColors.cardDisabledColor, + ), + ), + padding: + const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), + child: Text( + option, + style: AppTextStyles.blackTextStyle.copyWith( + color: + isSelected ? AppColors.whiteColor : AppColors.blackColor, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/learning/modules/exercises/widgets/see_progress_modal.dart b/lib/features/learning/modules/exercises/widgets/see_progress_modal.dart new file mode 100644 index 0000000..36a3550 --- /dev/null +++ b/lib/features/learning/modules/exercises/widgets/see_progress_modal.dart @@ -0,0 +1,93 @@ +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; + +class SeeProgressModal extends StatelessWidget { + const SeeProgressModal({super.key}); + + @override + Widget build(BuildContext context) { + final exerciseProvider = Provider.of(context); + final exercises = exerciseProvider.exercises; + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32.0, + vertical: 24.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Your Progress', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: const Icon(BootstrapIcons.x), + iconSize: 36, + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), + const Divider( + color: AppColors.disableColor, + thickness: 1, + ), + const SizedBox(height: 20), + GridView.builder( + shrinkWrap: true, // Important to wrap GridView inside a modal + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, // Number of columns + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 1, + ), + itemCount: exercises.length, + itemBuilder: (context, index) { + final isCompleted = exerciseProvider.isExerciseCompleted(index); + return ElevatedButton( + onPressed: () { + Navigator.pop(context); // Close modal + exerciseProvider + .goToExercise(index); // Navigate to the selected exercise + }, + style: ElevatedButton.styleFrom( + backgroundColor: + isCompleted ? AppColors.blueColor : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: const BorderSide( + color: AppColors.blueColor, + width: 1, + ), + ), + elevation: 0, // Remove shadow + padding: const EdgeInsets.all(0), + minimumSize: const Size(50, 50), + ), + child: Text( + '${index + 1}', + style: TextStyle( + color: isCompleted ? Colors.white : AppColors.blueColor, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/learning/modules/feedback/assets/images/feedback_illustration.svg b/lib/features/learning/modules/feedback/assets/images/feedback_illustration.svg new file mode 100644 index 0000000..f84d84e --- /dev/null +++ b/lib/features/learning/modules/feedback/assets/images/feedback_illustration.svg @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/learning/modules/feedback/screens/feedback_screen.dart b/lib/features/learning/modules/feedback/screens/feedback_screen.dart new file mode 100644 index 0000000..0e7961b --- /dev/null +++ b/lib/features/learning/modules/feedback/screens/feedback_screen.dart @@ -0,0 +1,163 @@ +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/learning/modules/feedback/widgets/feedback_dialog.dart'; +import 'package:flutter/material.dart'; + +class FeedbackScreen extends StatefulWidget { + const FeedbackScreen({super.key}); + + @override + State createState() => _FeedbackScreenState(); +} + +class _FeedbackScreenState extends State { + final FocusNode _focusNode = FocusNode(); + final TextEditingController _controller = TextEditingController(); + + @override + void initState() { + super.initState(); + _focusNode.addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + _focusNode.dispose(); + _controller.dispose(); + super.dispose(); + } + + void _unfocusTextField() { + _focusNode.unfocus(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _unfocusTextField, + child: Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + iconTheme: const IconThemeData(color: AppColors.whiteColor), + centerTitle: true, + title: Text( + 'Feedback', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + ), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + const SizedBox(height: 24), + Text( + 'Is there anything you would like to share after completing this exercise?', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 14, + ), + ), + const SizedBox(height: 16), + AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: _focusNode.hasFocus + ? AppColors.blueColor.withOpacity(0.3) + : Colors.transparent, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: TextField( + controller: _controller, + focusNode: _focusNode, + maxLines: 8, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 14, + ), + cursorColor: AppColors.blackColor, + decoration: InputDecoration( + hintText: 'Type your message here', + hintStyle: + AppTextStyles.greyTextStyle.copyWith(fontSize: 14), + filled: true, + fillColor: _focusNode.hasFocus + ? AppColors.whiteColor + : AppColors.whiteColor, + contentPadding: const EdgeInsets.all(16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: AppColors.blueColor, + width: 2, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppColors.disableColor.withOpacity(0.5)), + ), + ), + ), + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Send', + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return FeedbackDialog( + onSubmit: () { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => const LevelListScreen(), + // ), + // ); + }, + ); + }, + ); + }, + ), + const SizedBox(height: 16), + GlobalButton( + text: 'Skip', + textColor: AppColors.blueColor, + backgroundColor: Colors.transparent, + borderColor: AppColors.blueColor, + onPressed: () { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => const LevelListScreen(), + // ), + // ); + }, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/learning/modules/feedback/widgets/feedback_dialog.dart b/lib/features/learning/modules/feedback/widgets/feedback_dialog.dart new file mode 100644 index 0000000..9607c30 --- /dev/null +++ b/lib/features/learning/modules/feedback/widgets/feedback_dialog.dart @@ -0,0 +1,69 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class FeedbackDialog extends StatelessWidget { + final VoidCallback onSubmit; + + const FeedbackDialog({ + super.key, + required this.onSubmit, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Your Thoughts Matter!', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 20), + SvgPicture.asset( + 'lib/features/learning/modules/feedback/assets/images/feedback_illustration.svg', + width: 200, + ), + const SizedBox(height: 20), + Text( + 'Thank you for for taking the time to share your perspective. Your feedback is highly valued!', + textAlign: TextAlign.center, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 30), + GlobalButton( + text: 'Got It', + onPressed: () { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => const LevelListScreen(), + // ), + // ); + }, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/learning/modules/level/assets/images/Icon_Complete.png b/lib/features/learning/modules/level/assets/images/Icon_Complete.png new file mode 100644 index 0000000..b508f17 Binary files /dev/null and b/lib/features/learning/modules/level/assets/images/Icon_Complete.png differ diff --git a/lib/features/learning/modules/level/assets/images/complete_level.svg b/lib/features/learning/modules/level/assets/images/complete_level.svg new file mode 100644 index 0000000..7a4be5f --- /dev/null +++ b/lib/features/learning/modules/level/assets/images/complete_level.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/lib/features/learning/modules/level/assets/images/pretest_level_illustration.png b/lib/features/learning/modules/level/assets/images/pretest_level_illustration.png new file mode 100644 index 0000000..4d31837 Binary files /dev/null and b/lib/features/learning/modules/level/assets/images/pretest_level_illustration.png differ diff --git a/lib/features/learning/modules/level/models/level_model.dart b/lib/features/learning/modules/level/models/level_model.dart new file mode 100644 index 0000000..372b5b9 --- /dev/null +++ b/lib/features/learning/modules/level/models/level_model.dart @@ -0,0 +1,52 @@ +class Level { + final String idLevel; + final String idTopic; + final String idSection; + final String nameSection; + final String nameTopic; + final String nameLevel; + final String content; + final String? audio; + final String? image; + final String? video; + final int isPretest; + final String? timeLevel; + final String? idStudentLearning; + final int? score; + + const Level({ + required this.idLevel, + required this.idTopic, + required this.idSection, + required this.nameSection, + required this.nameTopic, + required this.nameLevel, + required this.content, + this.audio, + this.image, + this.video, + required this.isPretest, + required this.timeLevel, + this.idStudentLearning, + this.score, + }); + + factory Level.fromJson(Map json) { + return Level( + idLevel: json['ID_LEVEL'], + idTopic: json['ID_TOPIC'], + idSection: json['ID_SECTION'], + nameSection: json['NAME_SECTION'], + nameTopic: json['NAME_TOPIC'], + nameLevel: json['NAME_LEVEL'], + content: json['CONTENT'], + audio: json['AUDIO'], + image: json['IMAGE'], + video: json['VIDEO'], + isPretest: json['IS_PRETEST'], + timeLevel: json['TIME_LEVEL'], + idStudentLearning: json['ID_STUDENT_LEARNING'], + score: json['SCORE'], + ); + } +} diff --git a/lib/features/learning/modules/level/providers/level_provider.dart b/lib/features/learning/modules/level/providers/level_provider.dart new file mode 100644 index 0000000..c079861 --- /dev/null +++ b/lib/features/learning/modules/level/providers/level_provider.dart @@ -0,0 +1,91 @@ +import 'package:english_learning/core/services/repositories/level_repository.dart'; +import 'package:english_learning/features/learning/modules/level/models/level_model.dart'; +import 'package:flutter/foundation.dart'; + +class LevelProvider with ChangeNotifier { + final LevelRepository _levelRepository = LevelRepository(); + List _levels = []; + Map? _lastCompletedLevel; + bool _isLoading = false; + String? _error; + + List get levels => _levels; + Map? get lastCompletedLevel => _lastCompletedLevel; + bool get isLoading => _isLoading; + String? get error => _error; + + Future fetchLevels(String topicId, String token) async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + final result = await _levelRepository.getLevels(topicId, token); + _levels = result['levels']; + _lastCompletedLevel = result['lastCompletedLevel']; + if (_levels.isEmpty) { + _error = 'No levels found for this topic'; + } + } catch (e) { + _error = 'Error fetching levels: ${e.toString()}'; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + Level getPretest() { + return _levels.firstWhere((level) => level.isPretest == 1, + orElse: () => throw Exception('Pretest not found')); + } + + int getLevelScore(String levelId) { + final level = _levels.firstWhere((level) => level.idLevel == levelId, + orElse: () => const Level( + idLevel: '', + idTopic: '', + idSection: '', + nameSection: '', + nameTopic: '', + nameLevel: '', + content: '', + isPretest: 0, + timeLevel: '', + )); + return level.score ?? 0; + } + + bool isLevelAllowed(String levelId) { + if (_lastCompletedLevel == null) { + // Jika tidak ada level yang selesai, hanya pretest yang diizinkan + return levelId == _levels.first.idLevel; + } + + String lastCompletedLevelId = _lastCompletedLevel!['ID_LEVEL']; + int lastCompletedIndex = + _levels.indexWhere((level) => level.idLevel == lastCompletedLevelId); + int currentIndex = _levels.indexWhere((level) => level.idLevel == levelId); + + // Level diizinkan jika indeksnya kurang dari atau sama dengan indeks terakhir yang selesai + 1 + return currentIndex <= lastCompletedIndex + 1; + } + + // bool isLevelAllowed(int levelIndex) { + // if (levelIndex == 0) return true; // Pretest is always allowed + // if (_lastCompletedLevel == null) + // return levelIndex == + // 1; // If no level completed, only first level is allowed + // String lastCompletedLevelId = _lastCompletedLevel!['ID_LEVEL']; + // int lastCompletedIndex = + // _levels.indexWhere((level) => level.idLevel == lastCompletedLevelId); + // return levelIndex <= lastCompletedIndex + 2; + // } + + // bool isLevelFinished(int levelIndex) { + // return _levels[levelIndex].idStudentLearning != null; + // } + + // int getLevelScore(int levelIndex) { + // return _levels[levelIndex].score ?? 0; + // } +} diff --git a/lib/features/learning/modules/level/screens/level_list_screen.dart b/lib/features/learning/modules/level/screens/level_list_screen.dart new file mode 100644 index 0000000..9037b83 --- /dev/null +++ b/lib/features/learning/modules/level/screens/level_list_screen.dart @@ -0,0 +1,113 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/learning/modules/level/providers/level_provider.dart'; +import 'package:english_learning/features/learning/modules/level/widgets/level_card.dart'; +import 'package:english_learning/features/learning/modules/level/widgets/pretest_card.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class LevelListScreen extends StatefulWidget { + final String topicId; + final String topicTitle; + + const LevelListScreen({ + super.key, + required this.topicId, + required this.topicTitle, + }); + + @override + State createState() => _LevelListScreenState(); +} + +class _LevelListScreenState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final levelProvider = Provider.of(context, listen: false); + final userProvider = Provider.of(context, listen: false); + levelProvider.fetchLevels(widget.topicId, userProvider.jwtToken!); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + iconTheme: const IconThemeData(color: AppColors.whiteColor), + centerTitle: true, + title: RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'Level list of ', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + TextSpan( + text: widget.topicTitle, + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + ), + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + ), + ), + ), + body: Consumer( + builder: (context, levelProvider, child) { + if (levelProvider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (levelProvider.error != null) { + return Center(child: Text('No levels available')); + } else { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + PretestCard( + pretest: levelProvider.getPretest(), + score: levelProvider + .getLevelScore(levelProvider.getPretest().idLevel), + ), + const SizedBox(height: 12), + Expanded( + child: GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1.0, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + itemCount: levelProvider.levels.length - 1, + itemBuilder: (context, index) { + final level = levelProvider.levels[index + 1]; + return LevelCard( + level: level, + isAllowed: + levelProvider.isLevelAllowed(level.idLevel), + score: levelProvider.getLevelScore(level.idLevel), + ); + }, + ), + ), + ], + ), + ); + } + }, + ), + ); + } +} diff --git a/lib/features/learning/modules/level/widgets/level_card.dart b/lib/features/learning/modules/level/widgets/level_card.dart new file mode 100644 index 0000000..7a604f1 --- /dev/null +++ b/lib/features/learning/modules/level/widgets/level_card.dart @@ -0,0 +1,162 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/custom_button.dart'; +import 'package:english_learning/features/learning/modules/level/models/level_model.dart'; +import 'package:english_learning/features/learning/modules/material/screens/material_screen.dart'; +import 'package:flutter/material.dart'; + +class LevelCard extends StatelessWidget { + final Level level; + final bool isAllowed; + // final int level; + // final bool isAllowed; + // final bool isFinished; + final int score; + // final VoidCallback? onPressed; + const LevelCard({ + super.key, + required this.level, + required this.isAllowed, + // required this.level, + // required this.isAllowed, + // required this.isFinished, + required this.score, + // this.onPressed, + }); + + @override + Widget build(BuildContext context) { + bool isCompleted = level.idStudentLearning != null; + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + child: Container( + height: MediaQuery.of(context).size.height * 0.6, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: isAllowed + ? AppColors.gradientTheme + : const LinearGradient( + colors: [ + AppColors.disableColor, + AppColors.disableColor, + ], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + ), + child: Stack( + children: [ + if (isCompleted) + Positioned( + right: -23, + bottom: 4, + child: Image.asset( + 'lib/features/learning/modules/level/assets/images/Icon_Complete.png', + width: 110, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Level Header + Container( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppColors.whiteColor, + width: 1.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('LEVEL', + style: AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w900, + fontSize: 16, + )), + const SizedBox(width: 4), + Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(6), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 2.0, + ), + child: Text( + level.nameLevel.replaceAll('Level ', ''), + style: isAllowed + ? AppTextStyles.blueTextStyle.copyWith( + fontWeight: FontWeight.w900, + fontSize: 16, + ) + : AppTextStyles.disableTextStyle.copyWith( + fontWeight: FontWeight.w900, + fontSize: 16, + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 6), + // Score Display + Text( + // 'Score ${level.score}/100', + 'Score $score/100', + style: AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w900, + fontSize: 12, + ), + ), + const Spacer(), // Learn Now Button + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: CustomButton( + text: isAllowed ? 'Learn Now' : 'Not Allowed', + textStyle: + isAllowed ? null : AppTextStyles.disableTextStyle, + width: double.infinity, + height: 36, + color: isAllowed + ? AppColors.yellowButtonColor + : AppColors.cardButtonColor, + onPressed: isAllowed + ? () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MaterialScreen( + levelId: level.idLevel, + ), + ), + ); + } + : () {}, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/learning/modules/level/widgets/pretest_card.dart b/lib/features/learning/modules/level/widgets/pretest_card.dart new file mode 100644 index 0000000..027e9ce --- /dev/null +++ b/lib/features/learning/modules/level/widgets/pretest_card.dart @@ -0,0 +1,125 @@ +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/custom_button.dart'; +import 'package:english_learning/features/learning/modules/level/models/level_model.dart'; +import 'package:english_learning/features/learning/modules/material/screens/material_screen.dart'; +import 'package:flutter/material.dart'; + +class PretestCard extends StatelessWidget { + final Level pretest; + final int? score; + final VoidCallback? onPressed; + + const PretestCard({ + super.key, + required this.pretest, + this.score, + this.onPressed, + }); + + @override + Widget build(BuildContext context) { + bool isCompleted = pretest.idStudentLearning != null; + + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: AppColors.gradientTheme, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 4, horizontal: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: isCompleted ? Colors.green : Colors.transparent, + border: Border.all( + color: AppColors.whiteColor, + width: 1.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + pretest.nameLevel, + style: AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w900, + fontSize: 16, + ), + ), + if (isCompleted) ...[ + const SizedBox(width: 4), + const Icon( + BootstrapIcons.check_all, + color: AppColors.whiteColor, + size: 20, + ), + ], + ], + ), + ), + const SizedBox(height: 6), + Text( + 'Score $score/100', + style: AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w900, + fontSize: 12, + ), + ), + const SizedBox(height: 16), + ], + ), + Flexible( + child: Image.asset( + 'lib/features/learning/modules/level/assets/images/pretest_level_illustration.png', + height: 95, + fit: BoxFit.cover, + ), + ), + ], + ), + const SizedBox(height: 13), + CustomButton( + text: isCompleted ? 'Finished' : 'Learn Now', + textStyle: AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w900, + ), + width: double.infinity, + height: 36, + color: isCompleted ? Colors.green : AppColors.yellowButtonColor, + onPressed: () { + if (!isCompleted) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MaterialScreen( + levelId: pretest.idLevel, + ), + ), + ); + } + // Jika isCompleted true, tidak melakukan apa-apa + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/learning/modules/material/assets/images/material_illustration.png b/lib/features/learning/modules/material/assets/images/material_illustration.png new file mode 100644 index 0000000..295429b Binary files /dev/null and b/lib/features/learning/modules/material/assets/images/material_illustration.png differ diff --git a/lib/features/learning/modules/material/screens/material_screen.dart b/lib/features/learning/modules/material/screens/material_screen.dart new file mode 100644 index 0000000..6d47798 --- /dev/null +++ b/lib/features/learning/modules/material/screens/material_screen.dart @@ -0,0 +1,183 @@ +import 'package:english_learning/core/services/dio_client.dart'; +import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/core/services/repositories/student_learning_repository.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/learning/modules/exercises/screens/exercise_screen.dart'; +import 'package:english_learning/features/learning/modules/level/providers/level_provider.dart'; +import 'package:english_learning/features/learning/modules/material/widgets/audio_player_widget.dart'; +import 'package:english_learning/features/learning/modules/material/widgets/image_widget.dart'; +import 'package:english_learning/features/learning/modules/material/widgets/video_player_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:provider/provider.dart'; + +class MaterialScreen extends StatefulWidget { + final String levelId; + const MaterialScreen({ + super.key, + required this.levelId, + }); + + @override + State createState() => _MaterialScreenState(); +} + +class _MaterialScreenState extends State + with WidgetsBindingObserver { + bool _isLoading = false; + late StudentLearningRepository _repository; + final GlobalKey _videoPlayerKey = GlobalKey(); + final GlobalKey _audioPlayerKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _repository = StudentLearningRepository(DioClient()); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + _stopAndResetAllMedia(); + } + } + + void _stopAndResetAllMedia() { + _videoPlayerKey.currentState?.stopAndResetVideo(); + _audioPlayerKey.currentState?.stopAndResetAudio(); + } + + Future _createStudentLearning() async { + setState(() { + _isLoading = true; + }); + + try { + final userProvider = Provider.of(context, listen: false); + final token = await userProvider.getValidToken(); + + if (token == null) { + throw Exception('No valid token found'); + } + + final result = + await _repository.createStudentLearning(widget.levelId, token); + + print('Student Learning created: ${result['message']}'); + + // Navigate to ExerciseScreen + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ExerciseScreen( + levelId: widget.levelId, + studentLearningId: result['payload']['ID_STUDENT_LEARNING'], + ), + ), + ); + } catch (e) { + // Show error message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, levelProvider, child) { + final level = levelProvider.levels.firstWhere( + (level) => level.idLevel == widget.levelId, + orElse: () => throw Exception('Level not found'), + ); + return Scaffold( + appBar: AppBar( + elevation: 0, + iconTheme: const IconThemeData(color: AppColors.whiteColor), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + _stopAndResetAllMedia(); + Navigator.of(context).pop(); + }, + ), + title: Text( + '${level.nameTopic} - ${level.nameSection}', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w900, + ), + ), + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + ), + ), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + bottom: 16, + ), + child: Column( + children: [ + const SizedBox(height: 22), + Text( + level.content, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 24), + if (level.image != null) + ImageWidget( + imageFileName: level.image!, + baseUrl: '${baseUrl}uploads/level/image/', + ), + const SizedBox(height: 24), + if (level.audio != null) + AudioPlayerWidget( + key: _audioPlayerKey, + audioFileName: level.audio!, + baseUrl: '${baseUrl}uploads/level/audio/', + ), + const SizedBox(height: 24), + if (level.video != null) + VideoPlayerWidget( + key: _videoPlayerKey, + videoUrl: level.video!, + ), + const SizedBox(height: 32), + GlobalButton( + text: 'Take Pretest', + onPressed: _isLoading + ? null + : () { + _stopAndResetAllMedia(); + _createStudentLearning(); + }, + ) + ], + ), + ), + ), + ); + }); + } +} diff --git a/lib/features/learning/modules/material/widgets/audio_player_widget.dart b/lib/features/learning/modules/material/widgets/audio_player_widget.dart new file mode 100644 index 0000000..04ee36f --- /dev/null +++ b/lib/features/learning/modules/material/widgets/audio_player_widget.dart @@ -0,0 +1,209 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/material.dart'; + +class AudioPlayerWidget extends StatefulWidget { + final String? audioFileName; + final String baseUrl; + + const AudioPlayerWidget({ + super.key, + required this.audioFileName, + required this.baseUrl, + }); + + @override + State createState() => AudioPlayerWidgetState(); +} + +class AudioPlayerWidgetState extends State { + late AudioPlayer _audioPlayer; + PlayerState _playerState = PlayerState.stopped; + Duration _duration = Duration.zero; + Duration _position = Duration.zero; + bool _isAudioLoaded = false; + String? _errorMessage; + double _volume = 1.0; + + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _audioPlayer = AudioPlayer(); + _setupAudioPlayer(); + } + + void _setupAudioPlayer() { + if (widget.audioFileName != null && widget.audioFileName!.isNotEmpty) { + String fullAudioUrl = '${widget.baseUrl}${widget.audioFileName}'; + + _audioPlayer.setSource(UrlSource(fullAudioUrl)).then((_) { + setState(() { + _isAudioLoaded = true; + }); + }).catchError((error) { + setState(() { + _errorMessage = "Failed to load audio"; + }); + }); + + _audioPlayer.onPlayerStateChanged.listen((state) { + setState(() { + _playerState = state; + }); + }); + + _audioPlayer.onDurationChanged.listen((newDuration) { + setState(() { + _duration = newDuration; + }); + }); + + _audioPlayer.onPositionChanged.listen((newPosition) { + setState(() { + _position = newPosition; + }); + }); + } + } + + @override + void dispose() { + _audioPlayer.stop(); + _audioPlayer.dispose(); + super.dispose(); + } + + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, "0"); + String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); + String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); + return "$twoDigitMinutes:$twoDigitSeconds"; + } + + void stopAndResetAudio() { + _audioPlayer.stop(); + _audioPlayer.seek(Duration.zero); + } + + @override + Widget build(BuildContext context) { + if (_errorMessage != null) { + return Text( + _errorMessage!, + style: const TextStyle( + color: Colors.red, + ), + ); + } + + if (!_isAudioLoaded) { + return const CircularProgressIndicator(); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // Play/Pause Button + IconButton( + icon: Icon( + _playerState == PlayerState.playing + ? Icons.pause + : Icons.play_arrow, + color: Colors.blue, + size: 32, + ), + onPressed: () { + if (_playerState == PlayerState.playing) { + _audioPlayer.pause(); + } else { + _audioPlayer.resume(); + } + }, + ), + const SizedBox(width: 12), + // Timeline and Slider + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Timeline + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _formatDuration(_position), + style: const TextStyle( + fontSize: 12, + ), + ), + Text( + _formatDuration(_duration), + style: const TextStyle( + fontSize: 12, + ), + ), + ], + ), + const SizedBox(height: 4), + // Slider + SliderTheme( + data: const SliderThemeData( + trackHeight: 4, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6), + overlayShape: RoundSliderOverlayShape(overlayRadius: 14), + ), + child: Slider( + value: _position.inSeconds.toDouble(), + min: 0.0, + max: _duration.inSeconds.toDouble(), + onChanged: (value) { + final position = Duration(seconds: value.toInt()); + _audioPlayer.seek(position); + }, + activeColor: Colors.blue, + inactiveColor: Colors.grey, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + // Mute Button + IconButton( + icon: Icon( + Icons.volume_up, + color: _volume == 0.0 ? Colors.grey : Colors.blue, + size: 32, + ), + onPressed: () { + if (_volume == 0.0) { + _audioPlayer.setVolume(1.0); + setState(() { + _volume = 1.0; + }); + } else { + _audioPlayer.setVolume(0.0); + setState(() { + _volume = 0.0; + }); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/learning/modules/material/widgets/image_widget.dart b/lib/features/learning/modules/material/widgets/image_widget.dart new file mode 100644 index 0000000..460226b --- /dev/null +++ b/lib/features/learning/modules/material/widgets/image_widget.dart @@ -0,0 +1,50 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; + +class ImageWidget extends StatelessWidget { + final String imageFileName; + final String baseUrl; + + const ImageWidget({ + super.key, + required this.imageFileName, + required this.baseUrl, + }); + + @override + Widget build(BuildContext context) { + String fullImageUrl = '$baseUrl$imageFileName'; + + return Container( + width: double.infinity, + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: Offset(0, 5), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: CachedNetworkImage( + imageUrl: fullImageUrl, + fit: BoxFit.cover, + placeholder: (context, url) => + const Center(child: CircularProgressIndicator()), + errorWidget: (context, url, error) => Container( + color: Colors.grey[300], + child: const Icon( + Icons.error_outline, + color: Colors.red, + size: 50, + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/learning/modules/material/widgets/video_player_widget.dart b/lib/features/learning/modules/material/widgets/video_player_widget.dart new file mode 100644 index 0000000..37669da --- /dev/null +++ b/lib/features/learning/modules/material/widgets/video_player_widget.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; +import 'package:flick_video_player/flick_video_player.dart'; +import 'package:youtube_player_flutter/youtube_player_flutter.dart'; + +class VideoPlayerWidget extends StatefulWidget { + final String videoUrl; + + const VideoPlayerWidget({ + super.key, + required this.videoUrl, + }); + + @override + VideoPlayerWidgetState createState() => VideoPlayerWidgetState(); +} + +class VideoPlayerWidgetState extends State { + late Widget _videoWidget; + VideoPlayerController? _videoController; + YoutubePlayerController? _youtubeController; + FlickManager? _flickManager; + + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _initializeVideoPlayerWidget(); + } + + void _initializeVideoPlayerWidget() { + if (YoutubePlayer.convertUrlToId(widget.videoUrl) != null) { + _youtubeController = YoutubePlayerController( + initialVideoId: YoutubePlayer.convertUrlToId(widget.videoUrl)!, + flags: const YoutubePlayerFlags( + autoPlay: false, + mute: false, + ), + ); + + _videoWidget = YoutubePlayer( + controller: _youtubeController!, + showVideoProgressIndicator: true, + onReady: () { + _youtubeController!.addListener(_youtubeListener); + }, + ); + } else { + _videoController = + VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); + _flickManager = FlickManager( + videoPlayerController: _videoController!, + autoPlay: false, + ); + _videoWidget = FlickVideoPlayer( + flickManager: _flickManager!, + ); + } + } + + void _youtubeListener() { + if (_youtubeController!.value.playerState == PlayerState.ended) { + _youtubeController!.seekTo(Duration.zero); + _youtubeController!.pause(); + } + } + + @override + void dispose() { + _videoController?.dispose(); + _youtubeController?.dispose(); + _flickManager?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: AspectRatio( + aspectRatio: 16 / 9, + child: _videoWidget, + ), + ); + } + + void stopAndResetVideo() { + if (_youtubeController != null) { + _youtubeController!.seekTo(Duration.zero); + _youtubeController!.pause(); + } else if (_videoController != null) { + _videoController!.seekTo(Duration.zero); + _videoController!.pause(); + } + } +} diff --git a/lib/features/learning/modules/model/section_model.dart b/lib/features/learning/modules/model/section_model.dart new file mode 100644 index 0000000..229f407 --- /dev/null +++ b/lib/features/learning/modules/model/section_model.dart @@ -0,0 +1,25 @@ +class Section { + final String id; + final String name; + final String description; + final String thumbnail; + final DateTime timeSection; + + Section({ + required this.id, + required this.name, + required this.description, + required this.thumbnail, + required this.timeSection, + }); + + factory Section.fromJson(Map json) { + return Section( + id: json['ID_SECTION'], + name: json['NAME_SECTION'], + description: json['DESCRIPTION_SECTION'], + thumbnail: json['THUMBNAIL'], + timeSection: DateTime.parse(json['TIME_SECTION']), + ); + } +} diff --git a/lib/features/learning/modules/result/assets/images/result_complete_illustration.svg b/lib/features/learning/modules/result/assets/images/result_complete_illustration.svg new file mode 100644 index 0000000..4bc96a1 --- /dev/null +++ b/lib/features/learning/modules/result/assets/images/result_complete_illustration.svg @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/learning/modules/result/assets/images/result_down_illustration.svg b/lib/features/learning/modules/result/assets/images/result_down_illustration.svg new file mode 100644 index 0000000..f890180 --- /dev/null +++ b/lib/features/learning/modules/result/assets/images/result_down_illustration.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/learning/modules/result/assets/images/result_jump_illustration.svg b/lib/features/learning/modules/result/assets/images/result_jump_illustration.svg new file mode 100644 index 0000000..dde0657 --- /dev/null +++ b/lib/features/learning/modules/result/assets/images/result_jump_illustration.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/learning/modules/result/screens/result_screen.dart b/lib/features/learning/modules/result/screens/result_screen.dart new file mode 100644 index 0000000..d7a7200 --- /dev/null +++ b/lib/features/learning/modules/result/screens/result_screen.dart @@ -0,0 +1,53 @@ +import 'package:english_learning/features/learning/modules/result/widgets/complete_result_widget.dart'; +import 'package:english_learning/features/learning/modules/result/widgets/down_result_widget.dart'; +import 'package:english_learning/features/learning/modules/result/widgets/jump_result_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; + +class ResultScreen extends StatelessWidget { + final String currentLevel; + final String nextLevel; + final int score; + final bool? isCompleted; + + const ResultScreen({ + super.key, + required this.currentLevel, + required this.nextLevel, + required this.score, + required this.isCompleted, + }); + + @override + Widget build(BuildContext context) { + // final mediaQuery = MediaQuery.of(context); + // final screenHeight = mediaQuery.size.height; + + return Scaffold( + backgroundColor: AppColors.bgSoftColor, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (isCompleted!) + CompleteResultWidget( + currentLevel: currentLevel, + score: score, + ) + else if (nextLevel != currentLevel) + JumpResultWidget( + nextLevel: nextLevel, + score: score, + ) + else + DownResultWidget( + nextLevel: nextLevel, + score: score, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/learning/modules/result/widgets/complete_result_widget.dart b/lib/features/learning/modules/result/widgets/complete_result_widget.dart new file mode 100644 index 0000000..7946799 --- /dev/null +++ b/lib/features/learning/modules/result/widgets/complete_result_widget.dart @@ -0,0 +1,63 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class CompleteResultWidget extends StatelessWidget { + final String? currentLevel; + final int? score; + + const CompleteResultWidget({ + super.key, + required this.currentLevel, + required this.score, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + 'GREAT WORK', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 25, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 24), + SvgPicture.asset( + 'lib/features/learning/modules/result/assets/images/result_complete_illustration.svg', + width: 259, + ), + const SizedBox(height: 24), + Text( + 'Congratulations!', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 12), + Text( + 'Way to go! You conquered LEVEL $currentLevel with a $score/100! You\'re a rock star!', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Discover More', + onPressed: () { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => const FeedbackScreen(), + // ), + // ); + }, + ) + ], + ); + } +} diff --git a/lib/features/learning/modules/result/widgets/down_result_widget.dart b/lib/features/learning/modules/result/widgets/down_result_widget.dart new file mode 100644 index 0000000..07bfe36 --- /dev/null +++ b/lib/features/learning/modules/result/widgets/down_result_widget.dart @@ -0,0 +1,72 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class DownResultWidget extends StatelessWidget { + final String? nextLevel; + final int? score; + const DownResultWidget({ + super.key, + required this.nextLevel, + required this.score, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Your Result', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 25, + fontWeight: FontWeight.w900, + ), + ), + Text( + '$score/100', + style: AppTextStyles.redTextStyle.copyWith( + fontSize: 25, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + const SizedBox(height: 24), + SvgPicture.asset( + 'lib/features/learning/modules/result/assets/images/result_jump_illustration.svg', + width: 259, + ), + const SizedBox(height: 24), + Text( + 'Good effort! To improve, you can focus on!', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 8), + Text( + 'Level $nextLevel', + style: AppTextStyles.redTextStyle + .copyWith(fontSize: 20, fontWeight: FontWeight.w900), + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Continue', + onPressed: () { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => const FeedbackScreen(), + // ), + // ); + }, + ) + ], + ); + } +} diff --git a/lib/features/learning/modules/result/widgets/jump_result_widget.dart b/lib/features/learning/modules/result/widgets/jump_result_widget.dart new file mode 100644 index 0000000..c5ae8da --- /dev/null +++ b/lib/features/learning/modules/result/widgets/jump_result_widget.dart @@ -0,0 +1,74 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/features/learning/modules/feedback/screens/feedback_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class JumpResultWidget extends StatelessWidget { + final String? nextLevel; + final int? score; + + const JumpResultWidget({ + super.key, + required this.nextLevel, + required this.score, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Your Result', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 25, + fontWeight: FontWeight.w900, + ), + ), + Text( + '$score/100', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 20, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + const SizedBox(height: 24), + SvgPicture.asset( + 'lib/features/learning/modules/result/assets/images/result_jump_illustration.svg', + width: 259, + ), + const SizedBox(height: 24), + Text( + 'Great job! You can jump to ...', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 8), + Text( + '$nextLevel', + style: AppTextStyles.blueTextStyle + .copyWith(fontSize: 20, fontWeight: FontWeight.w900), + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Continue', + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const FeedbackScreen(), + ), + ); + }, + ) + ], + ); + } +} diff --git a/lib/features/learning/modules/topics/assets/images/listening_bg.png b/lib/features/learning/modules/topics/assets/images/listening_bg.png new file mode 100644 index 0000000..c673a9b Binary files /dev/null and b/lib/features/learning/modules/topics/assets/images/listening_bg.png differ diff --git a/lib/features/learning/modules/topics/models/topic_model.dart b/lib/features/learning/modules/topics/models/topic_model.dart new file mode 100644 index 0000000..0be62d8 --- /dev/null +++ b/lib/features/learning/modules/topics/models/topic_model.dart @@ -0,0 +1,22 @@ +class Topic { + final String id; + final String sectionId; + final String name; + final String description; + + Topic({ + required this.id, + required this.sectionId, + required this.name, + required this.description, + }); + + factory Topic.fromJson(Map json) { + return Topic( + id: json['ID_TOPIC'], + sectionId: json['ID_SECTION'], + name: json['NAME_TOPIC'], + description: json['DESCRIPTION_TOPIC'], + ); + } +} diff --git a/lib/features/learning/modules/topics/providers/topic_provider.dart b/lib/features/learning/modules/topics/providers/topic_provider.dart new file mode 100644 index 0000000..55efb1f --- /dev/null +++ b/lib/features/learning/modules/topics/providers/topic_provider.dart @@ -0,0 +1,29 @@ +import 'package:english_learning/core/services/repositories/topic_repository.dart'; +import 'package:flutter/foundation.dart'; +import 'package:english_learning/features/learning/modules/topics/models/topic_model.dart'; + +class TopicProvider extends ChangeNotifier { + final TopicRepository _topicRepository = TopicRepository(); + List _topics = []; + bool _isLoading = false; + String? _error; + + List get topics => _topics; + bool get isLoading => _isLoading; + String? get error => _error; + + Future fetchTopics(String sectionId, String token) async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + _topics = await _topicRepository.getTopics(sectionId, token); + } catch (e) { + _error = e.toString(); + } finally { + _isLoading = false; + notifyListeners(); + } + } +} diff --git a/lib/features/learning/modules/topics/screens/topics_list_screen.dart b/lib/features/learning/modules/topics/screens/topics_list_screen.dart new file mode 100644 index 0000000..3da23b9 --- /dev/null +++ b/lib/features/learning/modules/topics/screens/topics_list_screen.dart @@ -0,0 +1,170 @@ +import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/learning/modules/level/screens/level_list_screen.dart'; +import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart'; +import 'package:english_learning/features/learning/modules/topics/widgets/topic_card.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/learning/provider/section_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class TopicsListScreen extends StatefulWidget { + final String sectionId; + const TopicsListScreen({ + super.key, + required this.sectionId, + }); + + @override + State createState() => _TopicsListScreenState(); +} + +class _TopicsListScreenState extends State { + @override + void initState() { + super.initState(); + _fetchTopics(); + } + + String _getFullImageUrl(String thumbnail) { + if (thumbnail.startsWith('http')) { + return thumbnail; + } else { + return '${baseUrl}uploads/section/$thumbnail'; + } + } + + Future _fetchTopics() async { + final userProvider = Provider.of(context, listen: false); + final token = await userProvider.getValidToken(); + + if (token != null) { + await Provider.of(context, listen: false) + .fetchTopics(widget.sectionId, token); + } else { + // Handle the case when token is null (user might not be logged in) + print('No valid token found. User might need to log in.'); + } + } + + @override + Widget build(BuildContext context) { + final sectionProvider = Provider.of(context); + final selectedSection = sectionProvider.sections + .firstWhere((section) => section.id == widget.sectionId); + + return Scaffold( + backgroundColor: AppColors.bgSoftColor, + appBar: AppBar( + elevation: 0, + iconTheme: const IconThemeData(color: AppColors.whiteColor), + centerTitle: true, + title: Text( + selectedSection.name, + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w900, + ), + ), + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + ), + ), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Image.network( + _getFullImageUrl(selectedSection.thumbnail), + fit: BoxFit.cover, + width: double.infinity, + height: 115, + errorBuilder: (context, error, stackTrace) { + print('Error loading image: $error'); + return Container( + width: double.infinity, + height: 115, + color: Colors.grey[300], + child: Icon( + Icons.image_not_supported, + color: Colors.grey, + ), + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + width: 90, + height: 104, + color: Colors.grey[300], + child: Center(child: CircularProgressIndicator()), + ); + }, + ), + Container( + width: double.infinity, + height: 115, + color: AppColors.blackColor.withOpacity(0.5), + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 42.0), + child: Text( + selectedSection.description, + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + const SizedBox(height: 24), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Consumer( + builder: (context, topicProvider, _) { + if (topicProvider.topics.isEmpty) { + return Center(child: CircularProgressIndicator()); + } else if (topicProvider.error != null) { + return const Center(child: Text('No topics available')); + } else { + return ListView.builder( + itemCount: topicProvider.topics.length, + itemBuilder: (context, index) { + final topic = topicProvider.topics[index]; + return TopicCard( + title: topic.name, + description: topic.description, + isCompleted: + false, // You might want to implement completion tracking + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LevelListScreen( + topicId: topic.id, + topicTitle: topic.name, + ), + ), + ); + }, + ); + }, + ); + } + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/learning/modules/topics/widgets/topic_card.dart b/lib/features/learning/modules/topics/widgets/topic_card.dart new file mode 100644 index 0000000..a77b06b --- /dev/null +++ b/lib/features/learning/modules/topics/widgets/topic_card.dart @@ -0,0 +1,75 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:flutter/material.dart'; + +class TopicCard extends StatelessWidget { + final String title; + final String description; + final bool isCompleted; + final VoidCallback? onTap; + const TopicCard({ + super.key, + this.onTap, + required this.title, + required this.description, + required this.isCompleted, + }); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: GestureDetector( + onTap: onTap, + child: Card( + color: AppColors.whiteColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 16.0, + ), + child: Row( + // crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + maxLines: 4, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 8), + Icon( + isCompleted + ? Icons.check_circle + : Icons.radio_button_unchecked, + color: AppColors.blueColor, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/learning/provider/section_provider.dart b/lib/features/learning/provider/section_provider.dart new file mode 100644 index 0000000..8128873 --- /dev/null +++ b/lib/features/learning/provider/section_provider.dart @@ -0,0 +1,30 @@ +import 'package:english_learning/core/services/repositories/section_repository.dart'; +import 'package:english_learning/features/learning/modules/model/section_model.dart'; +import 'package:flutter/foundation.dart'; + +class SectionProvider extends ChangeNotifier { + final SectionRepository _repository = SectionRepository(); + List
_sections = []; + bool _isLoading = false; + String? _error; + + List
get sections => _sections; + bool get isLoading => _isLoading; + String? get error => _error; + + Future fetchSections(String token) async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + _sections = await _repository.getSections(token); + _isLoading = false; + notifyListeners(); + } catch (e) { + _isLoading = false; + _error = e.toString(); + notifyListeners(); + } + } +} diff --git a/lib/features/learning/screens/learning_screen.dart b/lib/features/learning/screens/learning_screen.dart new file mode 100644 index 0000000..3910a77 --- /dev/null +++ b/lib/features/learning/screens/learning_screen.dart @@ -0,0 +1,150 @@ +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/learning/provider/section_provider.dart'; +import 'package:english_learning/features/learning/widgets/section_card.dart'; +import 'package:english_learning/features/learning/modules/topics/screens/topics_list_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class LearningScreen extends StatefulWidget { + const LearningScreen({ + super.key, + }); + + @override + State createState() => _LearningScreenState(); +} + +class _LearningScreenState extends State { + @override + void initState() { + super.initState(); + _fetchSections(); + } + + Future _fetchSections() async { + final userProvider = Provider.of(context, listen: false); + final token = await userProvider.getValidToken(); + + if (token != null) { + await Provider.of(context, listen: false) + .fetchSections(token); + } else { + print('No valid token found. User might need to log in.'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgSoftColor, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 28.0, left: 16.0, right: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Choose what you want to learn!', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 8), + Text( + 'Develop your English skills by studying the topics in each section below.', + style: AppTextStyles.greyTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 15), + Expanded( + child: Consumer( + builder: (context, sectionProvider, _) { + if (sectionProvider.isLoading) { + return _buildShimmerLoading(); + } else if (sectionProvider.error != null) { + return Center( + child: Text('Error: ${sectionProvider.error}')); + } else { + return ListView.builder( + itemCount: sectionProvider.sections.length, + itemBuilder: (context, index) { + final section = sectionProvider.sections[index]; + return LearningCard( + section: section, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TopicsListScreen( + sectionId: section.id, + ), + ), + ), + ); + }, + ); + } + }, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildShimmerLoading() { + return ListView.builder( + itemCount: 5, // Misalnya, kita menampilkan 5 shimmer items + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + margin: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(8), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 20, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(8), + ), + ), + const SizedBox(height: 8), + Container( + height: 20, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(8), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/learning/widgets/section_card.dart b/lib/features/learning/widgets/section_card.dart new file mode 100644 index 0000000..0128e59 --- /dev/null +++ b/lib/features/learning/widgets/section_card.dart @@ -0,0 +1,100 @@ +import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/features/learning/modules/model/section_model.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; + +class LearningCard extends StatelessWidget { + final Section section; + final VoidCallback? onTap; + + const LearningCard({ + super.key, + required this.section, + this.onTap, + }); + + String _getFullImageUrl(String thumbnail) { + if (thumbnail.startsWith('http')) { + return thumbnail; + } else { + return '${baseUrl}uploads/section/$thumbnail'; + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Card( + color: AppColors.whiteColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 1, + margin: const EdgeInsets.symmetric(vertical: 6.0), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + _getFullImageUrl(section.thumbnail), + width: 90, + height: 104, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + print('Error loading image: $error'); + return Container( + width: 90, + height: 104, + color: Colors.grey[300], + child: Icon( + Icons.image_not_supported, + color: Colors.grey, + ), + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + width: 90, + height: 104, + color: Colors.grey[300], + child: Center(child: CircularProgressIndicator()), + ); + }, + )), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + section.name, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 4), + Text( + section.description, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + maxLines: 4, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/learning/widgets/section_card_shimmer.dart b/lib/features/learning/widgets/section_card_shimmer.dart new file mode 100644 index 0000000..0cfe126 --- /dev/null +++ b/lib/features/learning/widgets/section_card_shimmer.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class ShimmerWidget extends StatelessWidget { + final Widget child; + const ShimmerWidget({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: child, + ); + } +} diff --git a/lib/features/onboarding/data/onboarding_data.dart b/lib/features/onboarding/data/onboarding_data.dart index e1efab1..be7bce5 100644 --- a/lib/features/onboarding/data/onboarding_data.dart +++ b/lib/features/onboarding/data/onboarding_data.dart @@ -3,9 +3,9 @@ import 'package:english_learning/features/onboarding/models/onboarding_model.dar class OnBoardingData { List onBoardingData = [ OnBoardingModel( - title: 'Learn at Your Own Pace', + title: 'Interactive Lessons', description: - 'Customize your learning experience at a\nspeed that suits you.', + 'Customize your learning experience at a speed that suits you best.', image: 'lib/features/onboarding/assets/images/onboarding1.png', imageWidth: 250, imageHeight: 250, @@ -13,7 +13,7 @@ class OnBoardingData { OnBoardingModel( title: 'Tailored to You', description: - 'We ready to support you every step of\nyour English learning journey', + 'We ready to support you every step of your English learning journey', image: 'lib/features/onboarding/assets/images/onboarding2.png', imageWidth: 250, imageHeight: 201.17, @@ -21,7 +21,7 @@ class OnBoardingData { OnBoardingModel( title: 'Adaptive Learning', description: - 'Together, we achive more, Start your\npersonalized learning journey with us today!', + 'Together, we achive more, Start your personalized learning journey with us today!', image: 'lib/features/onboarding/assets/images/onboarding3.png', imageWidth: 250, imageHeight: 221.56, diff --git a/lib/features/onboarding/screens/onboarding_screen.dart b/lib/features/onboarding/screens/onboarding_screen.dart index e3f12a2..d69faa5 100644 --- a/lib/features/onboarding/screens/onboarding_screen.dart +++ b/lib/features/onboarding/screens/onboarding_screen.dart @@ -1,6 +1,8 @@ -import 'package:english_learning/features/auth/screens/signup_screen.dart'; +import 'package:english_learning/features/auth/screens/signup/signup_screen.dart'; import 'package:english_learning/features/onboarding/data/onboarding_data.dart'; -import 'package:english_learning/features/widgets/gradient_button.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/core/widgets/slider_widget.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:flutter/material.dart'; class OnBoardingScreen extends StatefulWidget { @@ -30,8 +32,8 @@ class _OnBoardingScreenState extends State { void _nextPage() { if (_currentPage < controller.onBoardingData.length - 1) { _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeIn, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, ); } else { // Handle what happens when the onboarding is complete @@ -41,14 +43,18 @@ class _OnBoardingScreenState extends State { void _skipToLastPage() { _pageController.animateToPage( controller.onBoardingData.length - 1, - duration: const Duration(milliseconds: 300), - curve: Curves.easeIn, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, ); } @override Widget build(BuildContext context) { + final mediaQuery = MediaQuery.of(context); + final screenHeight = mediaQuery.size.height; + return Scaffold( + backgroundColor: AppColors.whiteColor, body: Column( children: [ Expanded( @@ -59,69 +65,34 @@ class _OnBoardingScreenState extends State { itemBuilder: (context, index) { final item = controller.onBoardingData[index]; return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, vertical: 24.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 100), - ShaderMask( - shaderCallback: (bounds) => const LinearGradient( - colors: [ - Color(0xFF5674ED), - Color(0xFF34C3F9), - ], - ).createShader( - Rect.fromLTWH(0, 0, bounds.width, bounds.height), - ), - child: Text( - item.title, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), + SizedBox(height: screenHeight * 0.15), + Text( + item.title, + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 30, + fontWeight: FontWeight.w900, ), ), - const SizedBox(height: 16), + const SizedBox(height: 4), Text( item.description, - textAlign: TextAlign.center, - style: const TextStyle( + style: AppTextStyles.tetriaryTextStyle.copyWith( fontSize: 14, - fontWeight: FontWeight.w400, - height: 1.5, + fontWeight: FontWeight.w500, ), ), - const Spacer(flex: 2), - Image.asset( - item.image, - width: item.imageWidth, - height: item.imageHeight, - ), - const SizedBox(height: 70), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - controller.onBoardingData.length, - (index) => AnimatedContainer( - duration: const Duration(milliseconds: 300), - margin: const EdgeInsets.symmetric(horizontal: 4.0), - height: 8, - width: _currentPage == index ? 24 : 8, - decoration: BoxDecoration( - color: _currentPage == index - ? const Color(0xFF34C3F9) - : const Color(0xFFD8D8D8), - borderRadius: BorderRadius.circular(12), - ), - ), + SizedBox(height: screenHeight * 0.05), + Center( + child: Image.asset( + item.image, + width: item.imageWidth, + height: item.imageHeight, ), ), - const Spacer( - flex: 1), // Added Spacer for better alignment ], ), ); @@ -129,47 +100,79 @@ class _OnBoardingScreenState extends State { ), ), Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ + SliderWidget( + currentPage: _currentPage, + itemCount: controller.onBoardingData.length, + ), + SizedBox(height: screenHeight * 0.2), if (_currentPage == controller.onBoardingData.length - 1) Column( children: [ - GradientButton( - text: 'Sign Up', + GlobalButton( + text: 'Join Now', onPressed: () { Navigator.push( context, MaterialPageRoute( - builder: (context) => const SignupScreen()), + builder: (context) => SignupScreen()), ); }, ), - const SizedBox(height: 48), // Same height as the Spacer ], ) else - GradientButton( - text: 'Continue', - onPressed: _nextPage, - ), - const SizedBox( - height: 16), // Add some space between the buttons - if (_currentPage != controller.onBoardingData.length - 1) - TextButton( - onPressed: _skipToLastPage, - child: const Text( - 'Skip', - style: TextStyle( - color: Color(0xFF34C3F9), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppColors.disableColor, + width: 0.5, + ), + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + ), + onPressed: _skipToLastPage, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 14.0, + horizontal: 18.0, + ), + child: Text( + 'Skip', + style: AppTextStyles.disableTextStyle.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ), + ), ), - ), + const SizedBox(width: 12), + Expanded( + child: GlobalButton( + text: 'Continue', + icon: Icons.arrow_forward, + onPressed: _nextPage, + spaceBetween: true, + ), + ), + ], ), ], ), ), - const SizedBox(height: 50), + SizedBox(height: screenHeight * 0.1), ], ), ); diff --git a/lib/features/settings/modules/change_password/screens/change_password_screen.dart b/lib/features/settings/modules/change_password/screens/change_password_screen.dart new file mode 100644 index 0000000..2dd05e8 --- /dev/null +++ b/lib/features/settings/modules/change_password/screens/change_password_screen.dart @@ -0,0 +1,139 @@ +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/auth/provider/validator_provider.dart'; +import 'package:english_learning/core/widgets/form_field/custom_field_widget.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/auth/screens/signin/signin_screen.dart'; +import 'package:english_learning/features/settings/modules/change_password/widgets/change_password_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ChangePasswordScreen extends StatefulWidget { + const ChangePasswordScreen({super.key}); + + @override + State createState() => _ChangePasswordScreenState(); +} + +class _ChangePasswordScreenState extends State { + final TextEditingController _oldPasswordController = TextEditingController(); + final TextEditingController _newPasswordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); + + @override + void dispose() { + _oldPasswordController.dispose(); + _newPasswordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.whiteColor, + appBar: AppBar( + elevation: 0, + iconTheme: const IconThemeData(color: AppColors.whiteColor), + centerTitle: true, + title: Text( + 'Change Password', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + ), + ), + ), + body: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Consumer( + builder: (context, validatorProvider, child) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 22.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CustomFieldWidget( + isRequired: true, + labelText: 'Old Password', + hintText: 'Enter your old password', + textInputAction: TextInputAction.next, + fieldName: 'Old Password', + obscureText: true, + controller: _oldPasswordController, + ), + const SizedBox(height: 12), + CustomFieldWidget( + isRequired: true, + labelText: 'New Password', + hintText: 'Enter your new password', + textInputAction: TextInputAction.next, + fieldName: 'new password', + obscureText: true, + controller: _newPasswordController, + ), + const SizedBox(height: 12), + CustomFieldWidget( + isRequired: true, + labelText: 'Confirm New Password', + hintText: 'Retype your new password', + textInputAction: TextInputAction.next, + fieldName: 'confirm new password', + obscureText: true, + controller: _confirmPasswordController, + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Update Now', + onPressed: () async { + final userProvider = + Provider.of(context, listen: false); + bool success = await userProvider.updatePassword( + oldPassword: _oldPasswordController.text, + newPassword: _newPasswordController.text, + confirmPassword: _confirmPasswordController.text, + ); + + if (success) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return ChangePasswordDialog( + onSubmit: () { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => SigninScreen()), + (route) => false, + ); + }, + ); + }, + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Failed to update password. Please try again.')), + ); + } + }, + ) + ], + ), + ); + }, + )), + ); + } +} diff --git a/lib/features/settings/modules/change_password/widgets/change_password_dialog.dart b/lib/features/settings/modules/change_password/widgets/change_password_dialog.dart new file mode 100644 index 0000000..79def10 --- /dev/null +++ b/lib/features/settings/modules/change_password/widgets/change_password_dialog.dart @@ -0,0 +1,69 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ChangePasswordDialog extends StatelessWidget { + final VoidCallback onSubmit; + + const ChangePasswordDialog({ + super.key, + required this.onSubmit, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: 'Security Boosted!', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + SvgPicture.asset( + 'lib/features/auth/assets/images/forgot_password_illustration.svg', + width: 160, + ), + const SizedBox(height: 16), + Text( + 'Your password change was successful. You\'re good to go!', + textAlign: TextAlign.center, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Got it', + onPressed: onSubmit, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/settings/modules/edit_profile/screens/edit_profile_screen.dart b/lib/features/settings/modules/edit_profile/screens/edit_profile_screen.dart new file mode 100644 index 0000000..5801546 --- /dev/null +++ b/lib/features/settings/modules/edit_profile/screens/edit_profile_screen.dart @@ -0,0 +1,199 @@ +import 'dart:io'; +import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/form_field/custom_field_widget.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/settings/modules/edit_profile/widgets/save_changes_dialog.dart'; +import 'package:english_learning/features/settings/widgets/user_avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; + +class EditProfileScreen extends StatefulWidget { + const EditProfileScreen({super.key}); + + @override + State createState() => _EditProfileScreenState(); +} + +class _EditProfileScreenState extends State { + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _nisnController = TextEditingController(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final userProvider = Provider.of(context, listen: false); + _nameController.text = userProvider.userData?['NAME_USERS'] ?? ''; + _emailController.text = userProvider.userData?['EMAIL'] ?? ''; + _nisnController.text = userProvider.userData?['NISN']?.toString() ?? ''; + }); + } + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _nisnController.dispose(); + super.dispose(); + } + + Future _pickImage() async { + final pickedFile = + await ImagePicker().pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + final userProvider = Provider.of(context, listen: false); + userProvider.setSelectedImage(File(pickedFile.path)); + } + } + + Future _updateUserProfile(BuildContext context) async { + final userProvider = Provider.of(context, listen: false); + + final updatedData = { + 'NAME_USERS': _nameController.text, + 'EMAIL': _emailController.text, + 'NISN': _nisnController.text, + }; + + try { + await userProvider.updateUserProfile(updatedData); + + showDialog( + context: context, + builder: (BuildContext context) { + return SaveChangesDialog( + onSubmit: () { + Navigator.pop(context); // Close the dialog + Navigator.pop(context); // Go back to the previous screen + }, + ); + }, + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to update profile. Please try again.')), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + iconTheme: const IconThemeData(color: AppColors.whiteColor), + centerTitle: true, + title: Text( + 'Edit Profile', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + ), + ), + ), + body: Consumer(builder: (context, userProvider, child) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + alignment: Alignment.bottomCenter, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 50.0), + child: Container( + width: double.infinity, + height: 100, + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), + ), + ), + ), + GestureDetector( + child: UserAvatar( + radius: 60, + pictureUrl: userProvider.userData?['PICTURE'], + baseUrl: '$baseUrl/uploads/avatar/', + onImageSelected: _pickImage, + selectedImage: userProvider.selectedImage, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Change Avatar', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 39), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + CustomFieldWidget( + fieldName: 'nisn', + controller: _nisnController, + isEnabled: false, + isRequired: false, + textInputAction: TextInputAction.next, + labelText: 'NISN', + ), + const SizedBox(height: 20), + CustomFieldWidget( + fieldName: 'class', + isEnabled: false, + isRequired: false, + textInputAction: TextInputAction.next, + labelText: 'Class', + hintText: userProvider.userData?['NAME_CLASS']?.toString(), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 20), + CustomFieldWidget( + fieldName: 'fullname', + controller: _nameController, + isRequired: true, + textInputAction: TextInputAction.next, + labelText: 'Full Name', + keyboardType: TextInputType.text, + ), + const SizedBox(height: 20), + CustomFieldWidget( + fieldName: 'email', + controller: _emailController, + isRequired: true, + textInputAction: TextInputAction.next, + labelText: 'Email Address', + keyboardType: TextInputType.emailAddress, + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Save Changes', + onPressed: () => _updateUserProfile(context), + ), + ], + ), + ), + ], + )); + }), + ); + } +} diff --git a/lib/features/settings/modules/edit_profile/widgets/save_changes_dialog.dart b/lib/features/settings/modules/edit_profile/widgets/save_changes_dialog.dart new file mode 100644 index 0000000..5164ec3 --- /dev/null +++ b/lib/features/settings/modules/edit_profile/widgets/save_changes_dialog.dart @@ -0,0 +1,62 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class SaveChangesDialog extends StatelessWidget { + final VoidCallback onSubmit; + + const SaveChangesDialog({ + super.key, + required this.onSubmit, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Looking Sharp!', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 16), + SvgPicture.asset( + 'lib/features/auth/assets/images/forgot_password_illustration.svg', + width: 160, + ), + const SizedBox(height: 16), + Text( + 'Your profile has been successfully updated.', + textAlign: TextAlign.center, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 24), + GlobalButton( + text: 'Got It', + onPressed: onSubmit, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/settings/modules/logout/assets/images/logout_illustration.svg b/lib/features/settings/modules/logout/assets/images/logout_illustration.svg new file mode 100644 index 0000000..e16f41e --- /dev/null +++ b/lib/features/settings/modules/logout/assets/images/logout_illustration.svg @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/settings/modules/logout/screens/logout_confirmation.dart b/lib/features/settings/modules/logout/screens/logout_confirmation.dart new file mode 100644 index 0000000..8a8c8f6 --- /dev/null +++ b/lib/features/settings/modules/logout/screens/logout_confirmation.dart @@ -0,0 +1,101 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class LogoutConfirmation extends StatelessWidget { + final VoidCallback onSubmit; + + const LogoutConfirmation({ + super.key, + required this.onSubmit, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'Time to', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + TextSpan( + text: ' logout', + style: AppTextStyles.redTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + TextSpan( + text: '?', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + SvgPicture.asset( + 'lib/features/settings/modules/logout/assets/images/logout_illustration.svg', + width: 160, + ), + const SizedBox(height: 16), + Text( + 'Confirm logout? We\'ll be here when you return.', + textAlign: TextAlign.center, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 13), + Row( + children: [ + Expanded( + child: GlobalButton( + text: 'No', + textColor: AppColors.disableColor, + borderColor: AppColors.disableColor, + backgroundColor: Colors.transparent, + onPressed: () { + Navigator.of(context).pop(); + }), + ), + const SizedBox(width: 10), + Expanded( + child: GlobalButton( + text: 'Yes, logout!', + onPressed: onSubmit, + textColor: AppColors.whiteColor, + backgroundColor: AppColors.redColor, + ), + ), + ], + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/settings/modules/report_issue/assets/images/report_illustration.svg b/lib/features/settings/modules/report_issue/assets/images/report_illustration.svg new file mode 100644 index 0000000..18ccf57 --- /dev/null +++ b/lib/features/settings/modules/report_issue/assets/images/report_illustration.svg @@ -0,0 +1,705 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/settings/modules/report_issue/screens/report_issue_screen.dart b/lib/features/settings/modules/report_issue/screens/report_issue_screen.dart new file mode 100644 index 0000000..1edeec3 --- /dev/null +++ b/lib/features/settings/modules/report_issue/screens/report_issue_screen.dart @@ -0,0 +1,166 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/settings/modules/report_issue/widgets/report_issue_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ReportIssueScreen extends StatefulWidget { + const ReportIssueScreen({super.key}); + + @override + State createState() => _ReportIssueScreenState(); +} + +class _ReportIssueScreenState extends State { + final FocusNode _focusNode = FocusNode(); + final TextEditingController _controller = TextEditingController(); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _focusNode.addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + _focusNode.dispose(); + _controller.dispose(); + super.dispose(); + } + + void _unfocusTextField() { + _focusNode.unfocus(); + } + + Future _submitReport() async { + setState(() { + _isLoading = true; + }); + + final userProvider = Provider.of(context, listen: false); + final success = await userProvider.reportIssue(_controller.text); + + setState(() { + _isLoading = false; + }); + + if (success) { + showDialog( + context: context, + builder: (BuildContext context) { + return ReportIssueDialog( + onSubmit: () { + Navigator.of(context).pop(); // Close the dialog + Navigator.of(context).pop(); // Go back to previous screen + }, + ); + }, + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to submit report. Please try again.')), + ); + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _unfocusTextField, + child: Scaffold( + appBar: AppBar( + elevation: 0, + iconTheme: const IconThemeData(color: AppColors.whiteColor), + centerTitle: true, + title: Text( + 'Report an Issue', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + ), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + const SizedBox(height: 24), + Text( + 'In order to improve our service, kindly share your experience.', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 14, + ), + ), + const SizedBox(height: 8), + AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: _focusNode.hasFocus + ? AppColors.blueColor.withOpacity(0.3) + : Colors.transparent, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: TextField( + controller: _controller, + focusNode: _focusNode, + maxLines: 8, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 14, + ), + cursorColor: AppColors.blackColor, + decoration: InputDecoration( + hintText: 'Type your message here', + hintStyle: + AppTextStyles.greyTextStyle.copyWith(fontSize: 14), + filled: true, + fillColor: _focusNode.hasFocus + ? AppColors.whiteColor + : AppColors.whiteColor, + contentPadding: const EdgeInsets.all(16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: AppColors.blueColor, + width: 2, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppColors.disableColor.withOpacity(0.5)), + ), + ), + ), + ), + const SizedBox(height: 18), + GlobalButton( + text: 'Send', + onPressed: _isLoading ? null : _submitReport, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/settings/modules/report_issue/widgets/report_issue_dialog.dart b/lib/features/settings/modules/report_issue/widgets/report_issue_dialog.dart new file mode 100644 index 0000000..026cd6a --- /dev/null +++ b/lib/features/settings/modules/report_issue/widgets/report_issue_dialog.dart @@ -0,0 +1,82 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ReportIssueDialog extends StatelessWidget { + final VoidCallback onSubmit; + + const ReportIssueDialog({ + super.key, + required this.onSubmit, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'Issue Report', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + TextSpan( + text: ' Received', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + TextSpan( + text: '!', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + SvgPicture.asset( + 'lib/features/settings/modules/report_issue/assets/images/report_illustration.svg', + width: 160, + ), + const SizedBox(height: 16), + Text( + 'Thank you for letting us know. We\'ll investigate the issue and work on resolving it promptly.', + textAlign: TextAlign.center, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 13), + GlobalButton( + text: 'Got It', + onPressed: onSubmit, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/settings/screens/settings_screen.dart b/lib/features/settings/screens/settings_screen.dart new file mode 100644 index 0000000..a7f225d --- /dev/null +++ b/lib/features/settings/screens/settings_screen.dart @@ -0,0 +1,205 @@ +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/auth/screens/signin/signin_screen.dart'; +import 'package:english_learning/features/settings/modules/change_password/screens/change_password_screen.dart'; +import 'package:english_learning/features/settings/modules/edit_profile/screens/edit_profile_screen.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/settings/modules/logout/screens/logout_confirmation.dart'; +import 'package:english_learning/features/settings/modules/report_issue/screens/report_issue_screen.dart'; +import 'package:english_learning/features/settings/widgets/menu_item.dart'; +import 'package:english_learning/features/settings/widgets/user_avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({super.key}); + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + // @override + // void initState() { + // super.initState(); + // WidgetsBinding.instance.addPostFrameCallback((_) { + // Provider.of(context, listen: false).refreshUserData(); + // }); + // } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgSoftColor, + body: Consumer(builder: (context, userProvider, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Container( + width: double.infinity, + height: 210, + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), + ), + child: Padding( + padding: const EdgeInsets.only( + top: 71.0, left: 26, right: 16.0, bottom: 29.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + Row( + children: [ + UserAvatar( + pictureUrl: userProvider.userData?['PICTURE'], + baseUrl: '$baseUrl/uploads/avatar/', + ), + const SizedBox(width: 28), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + userProvider.userData?['NAME_USERS'] ?? + 'Loading...', + style: + AppTextStyles.whiteTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + userProvider.userData?['EMAIL'] ?? + 'Loading...', + style: + AppTextStyles.whiteTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ) + ], + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + MenuItem( + isFirst: true, + icon: BootstrapIcons.person_gear, + text: 'Edit Profile', + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const EditProfileScreen(), + ), + ); + Provider.of(context, listen: false) + .refreshUserData(); + }, + ), + const Divider( + color: AppColors.bgSoftColor, + height: 1, + thickness: 2, + ), + MenuItem( + icon: BootstrapIcons.shield_lock, + text: 'Change Password', + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ChangePasswordScreen(), + ), + ); + }, + ), + const Divider( + color: AppColors.bgSoftColor, + height: 1, + thickness: 2, + ), + MenuItem( + icon: BootstrapIcons.exclamation_circle, + text: 'Report an Issue', + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ReportIssueScreen(), + ), + ); + }, + ), + const Divider( + color: AppColors.bgSoftColor, + height: 1, + thickness: 2, + ), + MenuItem( + isLast: true, + icon: BootstrapIcons.box_arrow_right, + iconColor: AppColors.redColor, + text: 'Logout', + textColor: AppColors.redColor, + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return LogoutConfirmation( + onSubmit: () async { + final success = await userProvider.logout(); + if (success) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => SigninScreen()), + (Route route) => false, + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Logout failed. Please try again.')), + ); + } + }, + ); + }, + ); + }, + ), + ], + ), + ), + ) + ], + ); + }), + ); + } +} diff --git a/lib/features/settings/widgets/menu_item.dart b/lib/features/settings/widgets/menu_item.dart new file mode 100644 index 0000000..ea4a99e --- /dev/null +++ b/lib/features/settings/widgets/menu_item.dart @@ -0,0 +1,64 @@ +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:flutter/material.dart'; + +class MenuItem extends StatelessWidget { + final IconData icon; + final String text; + final VoidCallback onPressed; + final Color iconColor; + final Color textColor; + final bool isFirst; + final bool isLast; + + const MenuItem({ + super.key, + required this.icon, + required this.text, + required this.onPressed, + this.iconColor = AppColors.blackColor, + this.textColor = AppColors.blackColor, + this.isFirst = false, // Menandakan apakah ini item pertama + this.isLast = false, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: isFirst ? const Radius.circular(12) : Radius.zero, + topRight: isFirst ? const Radius.circular(12) : Radius.zero, + bottomLeft: isLast ? const Radius.circular(12) : Radius.zero, + bottomRight: isLast ? const Radius.circular(12) : Radius.zero, + ), + ), + padding: const EdgeInsets.symmetric( + vertical: 18.0, + horizontal: 16.0, + ), + ), + child: Row( + children: [ + Icon( + icon, + size: 18, + color: iconColor, + ), + const SizedBox(width: 8), + Text( + text, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w500, + color: textColor, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/settings/widgets/user_avatar.dart b/lib/features/settings/widgets/user_avatar.dart new file mode 100644 index 0000000..2f55793 --- /dev/null +++ b/lib/features/settings/widgets/user_avatar.dart @@ -0,0 +1,70 @@ +import 'dart:io'; + +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; + +class UserAvatar extends StatelessWidget { + final String? pictureUrl; + final double radius; + final String baseUrl; + final Function()? onImageSelected; + final File? selectedImage; + + const UserAvatar({ + super.key, + this.pictureUrl, + this.radius = 55, + required this.baseUrl, + this.onImageSelected, + this.selectedImage, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onImageSelected, + child: CircleAvatar( + radius: radius, + backgroundColor: AppColors.primaryColor, + child: _getAvatarContent(), + ), + ); + } + + Widget _getAvatarContent() { + if (selectedImage != null) { + return ClipOval( + child: Image.file( + selectedImage!, + fit: BoxFit.cover, + width: radius * 2, + height: radius * 2, + ), + ); + } else if (pictureUrl != null && pictureUrl!.isNotEmpty) { + return ClipOval( + child: Image.network( + '$baseUrl$pictureUrl', + fit: BoxFit.cover, + width: radius * 2, + height: radius * 2, + errorBuilder: (context, error, stackTrace) { + print('Error loading avatar image: $error'); + return _buildDefaultIcon(); + }, + ), + ); + } else { + return _buildDefaultIcon(); + } + } + + Widget _buildDefaultIcon() { + return Icon( + BootstrapIcons.person, + color: AppColors.whiteColor, + size: radius * 1.2, + ); + } +} diff --git a/lib/features/splash/assets/images/Logo.svg b/lib/features/splash/assets/images/Logo.svg new file mode 100644 index 0000000..72dbb30 --- /dev/null +++ b/lib/features/splash/assets/images/Logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/features/splash/assets/images/splash.png b/lib/features/splash/assets/images/splash.png new file mode 100644 index 0000000..8fdeb0d Binary files /dev/null and b/lib/features/splash/assets/images/splash.png differ diff --git a/lib/features/splash/screens/splash_screen.dart b/lib/features/splash/screens/splash_screen.dart index 185e32f..8b9e2ff 100644 --- a/lib/features/splash/screens/splash_screen.dart +++ b/lib/features/splash/screens/splash_screen.dart @@ -1,5 +1,12 @@ +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/auth/screens/signin/signin_screen.dart'; +import 'package:english_learning/features/home/screens/home_screen.dart'; import 'package:english_learning/features/welcome/screens/welcome_screen.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -8,18 +15,47 @@ class SplashScreen extends StatefulWidget { State createState() => _SplashScreenState(); } -class _SplashScreenState extends State - with SingleTickerProviderStateMixin { +class _SplashScreenState extends State { + bool _showLogo = true; + @override void initState() { super.initState(); + _checkLoginStatus(); + + Future.delayed(const Duration(seconds: 2), () { + setState(() { + _showLogo = false; + }); + }); + } + + Future _checkLoginStatus() async { + final userProvider = Provider.of(context, listen: false); + final prefs = await SharedPreferences.getInstance(); + + // Mengecek apakah sudah login + bool isLoggedIn = userProvider.isLoggedIn; + bool isFirstInstall = prefs.getBool('isFirstInstall') ?? true; Future.delayed(const Duration(seconds: 4), () { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (_) => const WelcomeScreen(), - ), - ); + if (isLoggedIn) { + // Jika sudah login, arahkan ke HomeScreen + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + } else if (isFirstInstall) { + // Jika pertama kali install, tampilkan WelcomeScreen + prefs.setBool('isFirstInstall', false); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const WelcomeScreen()), + ); + } else { + // Jika sudah pernah install tetapi belum login, arahkan ke SigninScreen + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => SigninScreen()), + ); + } }); } @@ -29,25 +65,43 @@ class _SplashScreenState extends State body: Container( width: double.infinity, decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [ - Color(0xFF5674ED), - Color(0xFF34C3F9), - ], + color: AppColors.blueColor, + image: DecorationImage( + image: AssetImage('lib/features/splash/assets/images/splash.png'), + fit: BoxFit.cover, ), ), - child: const Column( + child: Column( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - 'English Learning', - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: Colors.white, - ), + AnimatedSwitcher( + duration: const Duration(seconds: 1), + child: _showLogo + ? Row( + key: const ValueKey('logo'), + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + 'lib/features/splash/assets/images/Logo.svg', + width: 35, + ), + const SizedBox( + width: 3, + ), + Text( + 'SEALS', + style: AppTextStyles.logoTextStyle, + ), + ], + ) + : Text( + 'Smart\nEnglish\nAdaptive\nLearning\nSystem', + key: const ValueKey('text'), + style: AppTextStyles.logoTextStyle, + textAlign: TextAlign.left, + ), ), - SizedBox(height: 16), ], ), ), diff --git a/lib/features/welcome/screens/welcome_screen.dart b/lib/features/welcome/screens/welcome_screen.dart index 536b0bb..329dd00 100644 --- a/lib/features/welcome/screens/welcome_screen.dart +++ b/lib/features/welcome/screens/welcome_screen.dart @@ -1,6 +1,8 @@ -import 'package:english_learning/features/auth/screens/login_screen.dart'; +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:english_learning/features/auth/screens/signin/signin_screen.dart'; import 'package:english_learning/features/onboarding/screens/onboarding_screen.dart'; -import 'package:english_learning/features/widgets/gradient_button.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:flutter/material.dart'; class WelcomeScreen extends StatelessWidget { @@ -8,54 +10,53 @@ class WelcomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final mediaQuery = MediaQuery.of(context); + final screenHeight = mediaQuery.size.height; + return Scaffold( + backgroundColor: AppColors.whiteColor, body: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Spacer(flex: 2), - ShaderMask( - shaderCallback: (bounds) => const LinearGradient( - colors: [ - Color(0xFF5674ED), - Color(0xFF34C3F9), + SizedBox(height: screenHeight * 0.15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Hello!', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 32, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + 'Ready to start your learning journey?\nLet\'s get everything set up perfectly for you!', + style: AppTextStyles.greyTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 50), + const Center( + child: Image( + image: AssetImage( + 'lib/features/welcome/assets/images/welcome_illustration.png'), + width: 250, + height: 236.42, + ), + ), ], - ).createShader( - Rect.fromLTWH(0, 0, bounds.width, bounds.height), - ), - child: const Text( - 'Hello!', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 28, - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(height: 16), - const Text( - 'Ready to start your learning journey?\nLet\'s get everything set up perfectly for you!', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1.6, ), ), const Spacer(flex: 2), - const Image( - image: AssetImage( - 'lib/features/welcome/assets/images/welcome_illustration.png'), - width: 250, - height: 236.42, - ), - const Spacer(flex: 2), - GradientButton( + GlobalButton( text: 'Get Started', + icon: BootstrapIcons.arrow_right, + iconSize: 20, onPressed: () { Navigator.push( context, @@ -71,23 +72,24 @@ class WelcomeScreen extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( + Text( 'Already have an account? ', - style: TextStyle(color: Colors.black), + style: AppTextStyles.greyTextStyle.copyWith( + fontSize: 14, + ), ), GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( - builder: (context) => const LoginScreen()), + builder: (context) => SigninScreen()), ); }, - child: const Text( - 'Log In', - style: TextStyle( + child: Text( + 'Login', + style: AppTextStyles.blueTextStyle.copyWith( fontWeight: FontWeight.bold, - color: Color(0xFF34C3F9), ), ), ), @@ -95,7 +97,7 @@ class WelcomeScreen extends StatelessWidget { ), ], ), - const SizedBox(height: 50), + SizedBox(height: screenHeight * 0.1), ], ), ), diff --git a/lib/features/widgets/gradient_button.dart b/lib/features/widgets/gradient_button.dart deleted file mode 100644 index 5eab1b8..0000000 --- a/lib/features/widgets/gradient_button.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/material.dart'; - -class GradientButton extends StatelessWidget { - final String text; - final VoidCallback onPressed; - final double width; - final double height; - final List gradientColors; - - const GradientButton({ - super.key, - required this.text, - required this.onPressed, - this.width = double.infinity, - this.height = 50.0, - this.gradientColors = const [ - Color(0xFF5674ED), - Color(0xFF34C3F9), - ], - }); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: width, - height: height, - child: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: gradientColors, - ), - borderRadius: BorderRadius.circular(6), - ), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30.0), - ), - ), - onPressed: onPressed, - child: Text( - text, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - fontFamily: 'inter', - color: Colors.white, - height: 0.10, - ), - ), - ), - ), - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index f3bd6fb..915d15e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,25 +1,6 @@ -import 'package:english_learning/features/splash/screens/splash_screen.dart'; +import 'package:english_learning/my_app.dart'; import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; void main() { runApp(const MyApp()); } - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - theme: ThemeData().copyWith( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), - scaffoldBackgroundColor: Colors.white, - textTheme: GoogleFonts.interTextTheme(), - useMaterial3: true, - ), - home: const SplashScreen(), - ); - } -} diff --git a/lib/my_app.dart b/lib/my_app.dart new file mode 100644 index 0000000..af44e79 --- /dev/null +++ b/lib/my_app.dart @@ -0,0 +1,77 @@ +import 'package:english_learning/core/services/dio_client.dart'; +import 'package:english_learning/core/services/repositories/exercise_repository.dart'; +import 'package:english_learning/core/services/repositories/history_repository.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/auth/provider/validator_provider.dart'; +import 'package:english_learning/features/history/provider/history_provider.dart'; +import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; +import 'package:english_learning/features/learning/modules/level/providers/level_provider.dart'; +import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart'; +import 'package:english_learning/features/learning/provider/section_provider.dart'; +import 'package:english_learning/features/splash/screens/splash_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + Provider( + create: (_) => DioClient(), + ), + ChangeNotifierProvider(create: (_) => ValidatorProvider()), + ChangeNotifierProvider(create: (_) => TopicProvider()), + ChangeNotifierProvider(create: (_) => LevelProvider()), + ChangeNotifierProvider(create: (_) => SectionProvider()), + ChangeNotifierProvider(create: (_) => UserProvider()), + ProxyProvider( + update: (_, dioClient, __) => ExerciseRepository(dioClient), + ), + ProxyProvider( + update: (_, dioClient, __) => HistoryRepository(dioClient), + ), + ChangeNotifierProxyProvider2( + create: (context) => HistoryProvider( + context.read(), + context.read(), + ), + update: (context, historyRepository, sectionProvider, previous) => + HistoryProvider( + historyRepository, + sectionProvider, + ), + ), + ChangeNotifierProxyProvider2( + create: (context) => ExerciseProvider( + context.read(), + context.read(), + ), + update: (context, userProvider, exerciseRepository, previous) => + ExerciseProvider( + exerciseRepository, + userProvider, + )..updateFrom(previous), + ), + ], + child: Consumer( + builder: (context, userProvider, _) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: appTheme(), + home: const SplashScreen(), + ); + }, + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..598eb93 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,18 @@ #include "generated_plugin_registrant.h" +#include +#include +#include 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); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..ffd670e 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_linux + file_selector_linux + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index e777c67..b1cf832 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,26 @@ import FlutterMacOS import Foundation +import audioplayers_darwin +import file_selector_macos +import flutter_inappwebview_macos +import flutter_secure_storage_macos +import package_info_plus import path_provider_foundation +import shared_preferences_foundation +import sqflite +import video_player_avfoundation +import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 102473e..eded28f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: c346ba5a39dc208f1bab55fc239855f573d69b0e832402114bf0b793622adc4d + url: "https://pub.dev" + source: hosted + version: "6.1.0" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: de576b890befe27175c2f511ba8b742bec83765fa97c3ce4282bba46212f58e4 + url: "https://pub.dev" + source: hosted + version: "5.0.0" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: e507887f3ff18d8e5a10a668d7bedc28206b12e10b98347797257c6ae1019c3b + url: "https://pub.dev" + source: hosted + version: "6.0.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: "3d3d244c90436115417f170426ce768856d8fe4dfc5ed66a049d2890acfa82f9" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "6834dd48dfb7bc6c2404998ebdd161f79cd3774a7e6779e1348d54a3bfdcfaa5" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "3609bdf0e05e66a3d9750ee40b1e37f2a622c4edb796cc600b53a90a30a2ace4" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "8605762dddba992138d476f6a0c3afd9df30ac5b96039929063eceed416795c2" + url: "https://pub.dev" + source: hosted + version: "4.0.0" boolean_selector: dependency: transitive description: @@ -25,6 +81,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + bootstrap_icons: + dependency: "direct main" + description: + name: bootstrap_icons + sha256: c3f19c363ceadf5493532108c6db7e3112eef62a371a1b1eb5c67968d95a1aef + url: "https://pub.dev" + source: hosted + version: "1.11.3" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae" + url: "https://pub.dev" + source: hosted + version: "5.0.0" characters: dependency: transitive description: @@ -33,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" clock: dependency: transitive description: @@ -49,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -57,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" cupertino_icons: dependency: "direct main" description: @@ -65,6 +185,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + dio: + dependency: "direct main" + description: + name: dio + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.dev" + source: hosted + version: "2.0.0" fake_async: dependency: transitive description: @@ -81,11 +225,131 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flick_video_player: + dependency: "direct main" + description: + name: flick_video_player + sha256: f011cc28c6e4932485ac3a9d09ccacd25d9d430c99090480ccad2f5ebca9366e + url: "https://pub.dev" + source: hosted + version: "0.9.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_inappwebview: + dependency: transitive + description: + name: flutter_inappwebview + sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421 + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636 + url: "https://pub.dev" + source: hosted + version: "1.0.11" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07 + url: "https://pub.dev" + source: hosted + version: "1.0.8" flutter_lints: dependency: "direct dev" description: @@ -94,6 +358,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_logger_plus: + dependency: "direct main" + description: + name: flutter_logger_plus + sha256: c01751b8074384d116a4c1e4b549233053ea6b78fcc07b5503248227b06b923c + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" + url: "https://pub.dev" + source: hosted + version: "2.0.22" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" + url: "https://pub.dev" + source: hosted + version: "9.2.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_svg: dependency: "direct main" description: @@ -107,6 +435,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" google_fonts: dependency: "direct main" description: @@ -115,6 +448,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.1" + google_nav_bar: + dependency: "direct main" + description: + name: google_nav_bar + sha256: "1c8e3882fa66ee7b74c24320668276ca23affbd58f0b14a24c1e5590f4d07ab0" + url: "https://pub.dev" + source: hosted + version: "5.0.6" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" http: dependency: transitive description: @@ -131,6 +480,94 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8c5abf0dcc24fe6e8e0b4a5c0b51a5cf30cefdf6407a3213dae61edc75a70f56" + url: "https://pub.dev" + source: hosted + version: "0.8.12+12" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + url: "https://pub.dev" + source: hosted + version: "0.8.12" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + jwt_decoder: + dependency: "direct main" + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" leak_tracker: dependency: transitive description: @@ -163,6 +600,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + logger: + dependency: "direct main" + description: + name: logger + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" + url: "https://pub.dev" + source: hosted + version: "2.4.0" matcher: dependency: transitive description: @@ -187,6 +632,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.dev" + source: hosted + version: "1.0.6" nested: dependency: transitive description: @@ -195,6 +648,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 + url: "https://pub.dev" + source: hosted + version: "8.0.2" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + url: "https://pub.dev" + source: hosted + version: "3.0.1" path: dependency: transitive description: @@ -291,6 +768,78 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + url: "https://pub.dev" + source: hosted + version: "2.5.2" + 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: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -304,6 +853,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611" + url: "https://pub.dev" + source: hosted + version: "2.5.4+3" stack_trace: dependency: transitive description: @@ -328,6 +901,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc" + url: "https://pub.dev" + source: hosted + version: "3.3.0+2" term_glyph: dependency: transitive description: @@ -352,6 +933,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_html: + dependency: transitive + description: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + url: "https://pub.dev" + source: hosted + version: "4.5.0" vector_graphics: dependency: transitive description: @@ -384,6 +989,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d + url: "https://pub.dev" + source: hosted + version: "2.9.1" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: e343701aa890b74a863fa460f5c0e628127ed06a975d7d9af6b697133fb25bdf + url: "https://pub.dev" + source: hosted + version: "2.7.1" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c + url: "https://pub.dev" + source: hosted + version: "2.6.1" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774" + url: "https://pub.dev" + source: hosted + version: "2.3.2" vm_service: dependency: transitive description: @@ -392,6 +1037,22 @@ packages: url: "https://pub.dev" source: hosted version: "14.2.4" + wakelock_plus: + dependency: transitive + description: + name: wakelock_plus + sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 + url: "https://pub.dev" + source: hosted + version: "1.2.8" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" + url: "https://pub.dev" + source: hosted + version: "1.2.1" web: dependency: transitive description: @@ -400,6 +1061,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + url: "https://pub.dev" + source: hosted + version: "5.5.4" xdg_directories: dependency: transitive description: @@ -416,6 +1085,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + youtube_player_flutter: + dependency: "direct main" + description: + name: youtube_player_flutter + sha256: "30f84e2f7063c56e536f507e37c1e803546842707cf58e5b5a71253b9ff9b455" + url: "https://pub.dev" + source: hosted + version: "9.0.4" sdks: dart: ">=3.6.0-122.0.dev <4.0.0" flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4e41d4e..9646909 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,12 +28,30 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + audioplayers: ^6.1.0 + bootstrap_icons: ^1.11.3 + cached_network_image: ^3.4.1 + carousel_slider: ^5.0.0 cupertino_icons: ^1.0.8 + dio: ^5.7.0 + flick_video_player: ^0.9.0 flutter: sdk: flutter + flutter_cache_manager: ^3.4.1 + flutter_logger_plus: ^5.0.0 + flutter_secure_storage: ^9.2.2 flutter_svg: ^2.0.10+1 google_fonts: ^6.2.1 + google_nav_bar: ^5.0.6 + image_picker: ^1.1.2 + intl: ^0.19.0 + jwt_decoder: ^2.0.1 + logger: ^2.4.0 provider: ^6.1.2 + shared_preferences: ^2.3.2 + shimmer: ^3.0.0 + video_player: ^2.9.1 + youtube_player_flutter: ^9.0.4 dev_dependencies: flutter_lints: ^4.0.0 @@ -52,6 +70,19 @@ flutter: assets: - lib/features/welcome/assets/images/ - lib/features/onboarding/assets/images/ + - lib/features/splash/assets/images/ + - lib/features/auth/assets/images/ + - lib/features/home/assets/images/ + - lib/features/learning/assets/images/ + - lib/features/learning/modules/material/assets/images/ + - lib/features/learning/modules/exercises/assets/images/ + - lib/features/learning/modules/result/assets/images/ + - lib/features/learning/modules/level/assets/images/ + - lib/features/learning/modules/feedback/assets/images/ + - lib/features/settings/modules/report_issue/assets/images/ + - lib/features/settings/modules/logout/assets/images/ + - lib/features/splash/assets/images/ + - lib/features/history/assets/images/ # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images diff --git a/test/widget_test.dart b/test/widget_test.dart index 4bf66e8..46bff5a 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,11 +5,10 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. +import 'package:english_learning/my_app.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:english_learning/main.dart'; - void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..c40692c 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,15 @@ #include "generated_plugin_registrant.h" +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + AudioplayersWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..0388e99 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_windows + file_selector_windows + flutter_secure_storage_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST