diff --git a/lib/core/services/constants.dart b/lib/core/services/constants.dart new file mode 100644 index 0000000..84e4aa4 --- /dev/null +++ b/lib/core/services/constants.dart @@ -0,0 +1,2 @@ +const String baseUrl = + 'https://3be6-2001-448a-50a0-58e3-1c9f-405a-b360-22ed.ngrok-free.app/'; diff --git a/lib/core/services/dio_client.dart b/lib/core/services/dio_client.dart index 4893892..23c9d7d 100644 --- a/lib/core/services/dio_client.dart +++ b/lib/core/services/dio_client.dart @@ -1,7 +1,7 @@ // ignore_for_file: avoid_print import 'package:dio/dio.dart'; -import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/core/services/constants.dart'; class DioClient { final Dio _dio = Dio(); @@ -393,7 +393,7 @@ class DioClient { Future getStudentAnswers(String stdLearningId, String token) async { try { final response = await _dio.get( - '/studentAnswer/$stdLearningId', + '/studentAnswers/$stdLearningId', options: Options( headers: { 'Authorization': 'Bearer $token', diff --git a/lib/core/services/repositories/constants.dart b/lib/core/services/repositories/constants.dart deleted file mode 100644 index 722458b..0000000 --- a/lib/core/services/repositories/constants.dart +++ /dev/null @@ -1,2 +0,0 @@ -const String baseUrl = - 'https://70e7-2001-448a-50a0-2604-b558-2a22-54f6-65ce.ngrok-free.app/'; diff --git a/lib/core/services/repositories/exercise_repository.dart b/lib/core/services/repositories/exercise_repository.dart index 8d030f2..fb175da 100644 --- a/lib/core/services/repositories/exercise_repository.dart +++ b/lib/core/services/repositories/exercise_repository.dart @@ -25,7 +25,7 @@ class ExerciseRepository { try { final response = await _dioClient.getStudentAnswers(stdLearningId, token); if (response.statusCode == 200) { - return response.data['data']; + return response.data; } else { throw Exception('Failed to load student answers'); } diff --git a/lib/core/services/repositories/level_repository.dart b/lib/core/services/repositories/level_repository.dart index 5f1a6f6..7b8c338 100644 --- a/lib/core/services/repositories/level_repository.dart +++ b/lib/core/services/repositories/level_repository.dart @@ -32,20 +32,4 @@ class LevelRepository { rethrow; } } - - Future> getStudentAnswers( - String stdLearningId, String token) async { - try { - final response = await _dioClient.getStudentAnswers(stdLearningId, token); - - if (response.statusCode == 200 && response.data != null) { - return response.data['data']; - } else { - throw Exception( - 'Failed to fetch student answers: ${response.statusCode}'); - } - } catch (e) { - throw Exception('Failed to fetch student answers: $e'); - } - } } diff --git a/lib/core/widgets/global_button.dart b/lib/core/widgets/global_button.dart index f6a3cb6..41faefe 100644 --- a/lib/core/widgets/global_button.dart +++ b/lib/core/widgets/global_button.dart @@ -15,6 +15,7 @@ class GlobalButton extends StatelessWidget { final Color? borderColor; final double borderWidth; final bool transparentBackground; + final bool isLoading; const GlobalButton({ super.key, @@ -31,6 +32,7 @@ class GlobalButton extends StatelessWidget { this.borderColor, this.borderWidth = 1.0, this.transparentBackground = false, + this.isLoading = false, }); @override @@ -61,49 +63,66 @@ class GlobalButton extends StatelessWidget { ), ), 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, - ), - ), - ], - ), + child: isLoading ? _buildLoadingIndicator() : _buildButtonContent(), ), ), ); } + + Widget _buildLoadingIndicator() { + return SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(textColor ?? Colors.white), + strokeWidth: 2.0, + ), + ); + } + + Widget _buildButtonContent() { + if (spaceBetween && icon != null) { + return 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, + ), + ], + ); + } else { + return 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/features/auth/provider/user_provider.dart b/lib/features/auth/provider/user_provider.dart index 0c33162..8c23d52 100644 --- a/lib/features/auth/provider/user_provider.dart +++ b/lib/features/auth/provider/user_provider.dart @@ -9,7 +9,9 @@ class UserProvider with ChangeNotifier { String? _jwtToken; Map? _userData; File? _selectedImage; + bool _isLoading = false; + bool get isLoading => _isLoading; bool get isLoggedIn => _isLoggedIn; String? get jwtToken => _jwtToken; Map? get userData => _userData; @@ -19,6 +21,11 @@ class UserProvider with ChangeNotifier { _loadLoginStatus(); } + void setLoading(bool value) { + _isLoading = value; + notifyListeners(); + } + Future _loadLoginStatus() async { try { _jwtToken = await _userRepository.getToken(); @@ -60,6 +67,7 @@ class UserProvider with ChangeNotifier { } Future login({required String email, required String password}) async { + setLoading(true); try { final response = await _userRepository.loginUser({ 'EMAIL': email, @@ -84,6 +92,8 @@ class UserProvider with ChangeNotifier { } catch (e) { print('Login error: $e'); return false; + } finally { + setLoading(false); } } diff --git a/lib/features/auth/screens/signin/signin_screen.dart b/lib/features/auth/screens/signin/signin_screen.dart index 8241a63..bc8ef41 100644 --- a/lib/features/auth/screens/signin/signin_screen.dart +++ b/lib/features/auth/screens/signin/signin_screen.dart @@ -128,53 +128,58 @@ class SigninScreen extends StatelessWidget { 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, - ); + isLoading: userProvider.isLoading, + onPressed: userProvider.isLoading + ? null + : () 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 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, - ), - ); - } - } - }, + 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( @@ -187,6 +192,7 @@ class SigninScreen extends StatelessWidget { ), GestureDetector( onTap: () { + context.read().resetFields(); Navigator.push( context, MaterialPageRoute( diff --git a/lib/features/auth/screens/signup/signup_screen.dart b/lib/features/auth/screens/signup/signup_screen.dart index 4793622..748c8ff 100644 --- a/lib/features/auth/screens/signup/signup_screen.dart +++ b/lib/features/auth/screens/signup/signup_screen.dart @@ -227,13 +227,12 @@ class SignupScreen extends StatelessWidget { ), GestureDetector( onTap: () { + context.read().resetFields(); Navigator.push( context, MaterialPageRoute( builder: (context) => SigninScreen()), - ).then((_) { - context.read().resetFields(); - }); + ); }, child: Text( ' Login Here', diff --git a/lib/features/history/provider/history_provider.dart b/lib/features/history/provider/history_provider.dart index 4d91eb7..c82d9b5 100644 --- a/lib/features/history/provider/history_provider.dart +++ b/lib/features/history/provider/history_provider.dart @@ -51,6 +51,7 @@ class HistoryProvider with ChangeNotifier { _error = null; } catch (e) { _error = 'Error fetching learning history: ${e.toString()}'; + _learningHistory = []; } finally { _isLoading = false; notifyListeners(); @@ -91,10 +92,23 @@ class HistoryProvider with ChangeNotifier { } Future refreshData(String token) async { - if (_sectionProvider.sections.isEmpty) { + _isLoading = true; + _error = null; + notifyListeners(); + + try { await _sectionProvider.fetchSections(token); + if (_sectionProvider.sections.isNotEmpty) { + await fetchLearningHistory(token); + } else { + _error = 'No sections available'; + } + } catch (e) { + _error = 'Error refreshing data: ${e.toString()}'; + } finally { + _isLoading = false; + notifyListeners(); } - await fetchLearningHistory(token); } Future loadHistoryData(String token) async { @@ -103,29 +117,52 @@ class HistoryProvider with ChangeNotifier { notifyListeners(); try { - // Fetch sections and learning history in parallel - final sectionsResult = _sectionProvider.fetchSections(token); - - // Use the first section ID for initial history fetch - final firstSectionId = await sectionsResult - .then((sections) => sections.isNotEmpty ? sections.first.id : null); - - if (firstSectionId != null) { - final historyResult = - _repository.getLearningHistory(firstSectionId, token); - - // Wait for both futures to complete - final results = await Future.wait([sectionsResult, historyResult]); - - _learningHistory = results[1] as List; + await _sectionProvider.fetchSections(token); + if (_sectionProvider.sections.isNotEmpty) { + String firstSectionId = _sectionProvider.sections.first.id; + _learningHistory = + await _repository.getLearningHistory(firstSectionId, token); } else { _error = 'No sections available'; } } catch (e) { _error = 'Error loading data: ${e.toString()}'; + _learningHistory = []; // Clear the list in case of error } finally { _isLoading = false; notifyListeners(); } } + + // Future loadHistoryData(String token) async { + // _isLoading = true; + // _error = null; + // notifyListeners(); + + // try { + // // Fetch sections and learning history in parallel + // final sectionsResult = _sectionProvider.fetchSections(token); + + // // Use the first section ID for initial history fetch + // final firstSectionId = await sectionsResult + // .then((sections) => sections.isNotEmpty ? sections.first.id : null); + + // if (firstSectionId != null) { + // final historyResult = + // _repository.getLearningHistory(firstSectionId, token); + + // // Wait for both futures to complete + // final results = await Future.wait([sectionsResult, historyResult]); + + // _learningHistory = results[1] as List; + // } else { + // _error = 'No sections available'; + // } + // } catch (e) { + // _error = 'Error loading data: ${e.toString()}'; + // } finally { + // _isLoading = false; + // notifyListeners(); + // } + // } } diff --git a/lib/features/history/screens/history_screen.dart b/lib/features/history/screens/history_screen.dart index 9c81b12..161bbba 100644 --- a/lib/features/history/screens/history_screen.dart +++ b/lib/features/history/screens/history_screen.dart @@ -10,76 +10,50 @@ 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((_) { - final historyProvider = - Provider.of(context, listen: false); - final userProvider = Provider.of(context, listen: false); - historyProvider.fetchLearningHistory(userProvider.jwtToken!); - }); - } +class HistoryScreen extends StatelessWidget { + const HistoryScreen({super.key}); bool isNotFoundError(String error) { return error.toLowerCase().contains('no learning history found') || error.toLowerCase().contains('not found'); } - // @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 Consumer( - builder: (context, historyProvider, chiild) { - return Scaffold( - backgroundColor: AppColors.bgSoftColor, - body: SafeArea( - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 30.0, - ), - child: Column( - children: [ - _buildHeader(), - const SizedBox(height: 8), - const CustomTabBar(), - const SizedBox(height: 8), - Expanded( - child: _buildContent(historyProvider), - ), - ], + builder: (context, historyProvider, child) { + return Scaffold( + backgroundColor: AppColors.bgSoftColor, + body: SafeArea( + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 30.0, + ), + child: Column( + children: [ + _buildHeader(), + const SizedBox(height: 8), + const CustomTabBar(), + const SizedBox(height: 8), + Expanded( + child: _buildContent(context, historyProvider), + ), + ], + ), ), ), ), - ), - ); - }); + ); + }, + ); } - Widget _buildContent(HistoryProvider historyProvider) { + Widget _buildContent(BuildContext context, HistoryProvider historyProvider) { + print("isLoading: ${historyProvider.isLoading}"); + print("error: ${historyProvider.error}"); + print("historyLength: ${historyProvider.learningHistory.length}"); if (historyProvider.isLoading) { return const Center(child: CircularProgressIndicator()); } @@ -87,7 +61,7 @@ class _HistoryScreenState extends State { if (historyProvider.error != null) { return isNotFoundError(historyProvider.error!) ? _buildEmptyState(context) - : _buildErrorState(historyProvider.error!); + : _buildErrorState(context, historyProvider.error!); } if (historyProvider.learningHistory.isEmpty) { @@ -145,7 +119,7 @@ class _HistoryScreenState extends State { ); } - Widget _buildErrorState(String error) { + Widget _buildErrorState(BuildContext context, String error) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart index 0ddc28c..df5eedc 100644 --- a/lib/features/home/screens/home_screen.dart +++ b/lib/features/home/screens/home_screen.dart @@ -206,6 +206,22 @@ class _HomeContentState extends State { ); } + Widget _buildCompletedTopicsContent(CompletedTopicsProvider provider) { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + itemCount: 1, + itemBuilder: (context, index) { + return ProgressCard( + completedTopic: provider.completedTopics, + ); + }, + ); + } + @override Widget build(BuildContext context) { return Consumer2(builder: ( @@ -403,20 +419,27 @@ class _HomeContentState extends State { ? _buildShimmerEffect() : completedTopicsProvider.completedTopics.isEmpty ? _buildNoDataWidget() - : ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - ), - itemCount: 1, - itemBuilder: (context, index) { - return ProgressCard( - completedTopic: completedTopicsProvider - .completedTopics, // Kirim seluruh list - ); - }, - ), + : _buildCompletedTopicsContent( + completedTopicsProvider), + + // completedTopicsProvider.isLoading + // ? _buildShimmerEffect() + // : completedTopicsProvider.completedTopics.isEmpty + // ? _buildNoDataWidget() + // : ListView.builder( + // shrinkWrap: true, + // physics: const NeverScrollableScrollPhysics(), + // padding: const EdgeInsets.symmetric( + // horizontal: 16.0, + // ), + // itemCount: 1, + // itemBuilder: (context, index) { + // return ProgressCard( + // completedTopic: completedTopicsProvider + // .completedTopics, // Kirim seluruh list + // ); + // }, + // ), ], ), ), diff --git a/lib/features/home/widgets/progress_card.dart b/lib/features/home/widgets/progress_card.dart index 7607ca1..83618d0 100644 --- a/lib/features/home/widgets/progress_card.dart +++ b/lib/features/home/widgets/progress_card.dart @@ -1,5 +1,4 @@ -import 'package:bootstrap_icons/bootstrap_icons.dart'; -import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/core/services/constants.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/features/home/models/completed_topics_model.dart'; import 'package:english_learning/features/home/widgets/progress_bar.dart'; diff --git a/lib/features/learning/modules/exercises/models/exercise_model.dart b/lib/features/learning/modules/exercises/models/exercise_model.dart index 3172bcd..e7b1eaa 100644 --- a/lib/features/learning/modules/exercises/models/exercise_model.dart +++ b/lib/features/learning/modules/exercises/models/exercise_model.dart @@ -1,3 +1,5 @@ +import 'package:english_learning/features/learning/modules/exercises/models/review_exercise_model.dart'; + class ExerciseModel { final String idAdminExercise; final String idLevel; @@ -61,21 +63,6 @@ class ExerciseModel { choices: choices, ); } - - factory ExerciseModel.fromReviewJson(Map json) { - final exerciseDetails = json['exerciseDetails']; - return ExerciseModel( - idAdminExercise: exerciseDetails['ID_ADMIN_EXERCISE'], - idLevel: json['ID_LEVEL'] ?? '', // This might be available in parent data - title: exerciseDetails['TITLE'], - question: exerciseDetails['QUESTION'], - questionType: exerciseDetails['QUESTION_TYPE'], - timeAdminExc: DateTime.parse(json['TIME_STUDENT_EXC']), - answerStudent: json['ANSWER_STUDENT'], - isCorrect: json['IS_CORRECT'] == 1, - choices: null, - ); - } } class MultipleChoice { @@ -138,6 +125,13 @@ class Pair { final String right; Pair({required this.left, required this.right}); + + factory Pair.fromJson(Map json) { + return Pair( + left: json['LEFT_PAIR'] ?? '', + right: json['RIGHT_PAIR'] ?? '', + ); + } } class MatchingPair { @@ -147,17 +141,24 @@ class MatchingPair { factory MatchingPair.fromJsonList(List? jsonList) { if (jsonList == null) { - return MatchingPair( - pairs: []); // Return an empty list if jsonList is null + return MatchingPair(pairs: []); } - 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'] ?? '', - )); - } + List pairs = jsonList.map((item) => Pair.fromJson(item)).toList(); + return MatchingPair(pairs: pairs); + } + + // Helper methods + List get leftPairs => pairs.map((pair) => pair.left).toList(); + List get rightPairs => pairs.map((pair) => pair.right).toList(); +} + +extension ReviewMatchingPairListExtension on List { + MatchingPair toMatchingPair() { + List pairs = map((review) => Pair( + left: review.leftPair, + right: review.rightPair, + )).toList(); return MatchingPair(pairs: pairs); } } diff --git a/lib/features/learning/modules/exercises/models/review_exercise_model.dart b/lib/features/learning/modules/exercises/models/review_exercise_model.dart new file mode 100644 index 0000000..fd78585 --- /dev/null +++ b/lib/features/learning/modules/exercises/models/review_exercise_model.dart @@ -0,0 +1,150 @@ +class ReviewExerciseModel { + final String idStudentLearning; + final String idLevel; + final String nameSection; + final String nameTopic; + final String nameLevel; + final int score; + final bool isPass; + final List stdExercises; + + ReviewExerciseModel({ + required this.idStudentLearning, + required this.idLevel, + required this.nameSection, + required this.nameTopic, + required this.nameLevel, + required this.score, + required this.isPass, + required this.stdExercises, + }); + + factory ReviewExerciseModel.fromJson(Map json) { + return ReviewExerciseModel( + idStudentLearning: json['ID_STUDENT_LEARNING'], + idLevel: json['ID_LEVEL'], + nameSection: json['NAME_SECTION'], + nameTopic: json['NAME_TOPIC'], + nameLevel: json['NAME_LEVEL'], + score: json['SCORE'], + isPass: json['IS_PASS'] == 1, + stdExercises: (json['stdExercises'] as List) + .map((e) => ReviewExerciseDetail.fromJson(e)) + .toList(), + ); + } +} + +class ReviewExerciseDetail { + final String idStudentExercise; + final String idAdminExercise; + final String title; + final String question; + final String questionType; + final int scoreWeight; + final String? image; + final String? video; + final String? audio; + + final String answerStudent; + final int isCorrect; + final double resultScoreStudent; + final List? multipleChoices; + final List? matchingPairs; + + ReviewExerciseDetail({ + required this.idStudentExercise, + required this.idAdminExercise, + required this.title, + required this.question, + required this.questionType, + this.image, + this.video, + this.audio, + required this.scoreWeight, + required this.answerStudent, + required this.isCorrect, + required this.resultScoreStudent, + this.multipleChoices, + this.matchingPairs, + }); + + factory ReviewExerciseDetail.fromJson(Map json) { + return ReviewExerciseDetail( + idStudentExercise: json['ID_STUDENT_EXERCISE'] ?? '', + idAdminExercise: json['ID_ADMIN_EXERCISE'] ?? '', + title: json['TITLE'] ?? '', + question: json['QUESTION'] ?? '', + questionType: json['QUESTION_TYPE'] ?? '', + image: json['IMAGE'], + video: json['VIDEO'], + audio: json['AUDIO'], + scoreWeight: json['SCORE_WEIGHT'] ?? 0, + answerStudent: json['ANSWER_STUDENT'] ?? '', + isCorrect: json['IS_CORRECT'] ?? 0, + resultScoreStudent: + double.tryParse(json['RESULT_SCORE_STUDENT'].toString()) ?? 0.0, + multipleChoices: json['multipleChoices'] != null + ? (json['multipleChoices'] as List) + .map((e) => ReviewMultipleChoice.fromJson(e)) + .toList() + : null, + matchingPairs: json['matchingPairs'] != null + ? (json['matchingPairs'] as List) + .map((e) => ReviewMatchingPair.fromJson(e)) + .toList() + : null, + ); + } + + List get matchingPairsOrEmpty { + return matchingPairs ?? []; + } + + List get multipleChoicesOrEmpty { + return multipleChoices ?? []; + } +} + +class ReviewMultipleChoice { + final String optionA; + final String optionB; + final String optionC; + final String optionD; + final String optionE; + + ReviewMultipleChoice({ + required this.optionA, + required this.optionB, + required this.optionC, + required this.optionD, + required this.optionE, + }); + + factory ReviewMultipleChoice.fromJson(Map json) { + return ReviewMultipleChoice( + optionA: json['OPTION_A'] ?? '', + optionB: json['OPTION_B'] ?? '', + optionC: json['OPTION_C'] ?? '', + optionD: json['OPTION_D'] ?? '', + optionE: json['OPTION_E'] ?? '', + ); + } +} + +class ReviewMatchingPair { + final String leftPair; + final String rightPair; + + ReviewMatchingPair({ + required this.leftPair, + required this.rightPair, + }); + + factory ReviewMatchingPair.fromJson(Map json) { + return ReviewMatchingPair( + leftPair: json['LEFT_PAIR'], + rightPair: json['RIGHT_PAIR'], + ); + } +} diff --git a/lib/features/learning/modules/exercises/providers/exercise_provider.dart b/lib/features/learning/modules/exercises/providers/exercise_provider.dart index 9a01935..8e0f9c2 100644 --- a/lib/features/learning/modules/exercises/providers/exercise_provider.dart +++ b/lib/features/learning/modules/exercises/providers/exercise_provider.dart @@ -1,3 +1,4 @@ +import 'package:english_learning/features/learning/modules/exercises/models/review_exercise_model.dart'; import 'package:english_learning/features/learning/modules/feedback/models/feedback_model.dart'; import 'package:flutter/material.dart'; import 'package:english_learning/core/services/repositories/exercise_repository.dart'; @@ -14,6 +15,8 @@ class ExerciseProvider extends ChangeNotifier { // State variables List _exercises = []; + List _reviewExercises = []; + ReviewExerciseModel? _reviewData; Map>> _matchingAnswers = {}; List _answers = []; List> _leftColors = []; @@ -24,7 +27,6 @@ class ExerciseProvider extends ChangeNotifier { String _nameLevel = ''; String? _activeLeftOption; String? _studentLearningId; - bool _isReview = false; // Constants final List _pairColors = [ @@ -44,7 +46,7 @@ class ExerciseProvider extends ChangeNotifier { String get nameLevel => _nameLevel; String? get activeLeftOption => _activeLeftOption; String? get studentLearningId => _studentLearningId; - bool get isReview => _isReview; + List get reviewExercises => _reviewExercises; // Initialization methods void initializeAnswers() { @@ -79,7 +81,6 @@ class ExerciseProvider extends ChangeNotifier { // } // } void answerQuestion(int index, String answer) { - if (_isReview) return; if (index >= 0 && index < _answers.length) { if (_exercises[index].choices is MatchingPair) { _handleMatchingPairAnswer(index, answer); @@ -208,20 +209,30 @@ class ExerciseProvider extends ChangeNotifier { void goToExercise(int index) { if (index >= 0 && index < _exercises.length) { _currentExerciseIndex = index; + print('Going to exercise: $_currentExerciseIndex'); // Debug print notifyListeners(); } } void nextQuestion() { - if (_currentExerciseIndex < _answers.length - 1) { + print('Current index before next: $_currentExerciseIndex'); // Debug print + if (_currentExerciseIndex < + (_exercises.isNotEmpty + ? _exercises.length - 1 + : _reviewExercises.length - 1)) { _currentExerciseIndex++; + print('Moving to next question: $_currentExerciseIndex'); // Debug print notifyListeners(); } } void previousQuestion() { + print( + 'Current index before previous: $_currentExerciseIndex'); // Debug print if (_currentExerciseIndex > 0) { _currentExerciseIndex--; + print( + 'Moving to previous question: $_currentExerciseIndex'); // Debug print notifyListeners(); } } @@ -245,6 +256,7 @@ class ExerciseProvider extends ChangeNotifier { // API methods Future fetchExercises(String levelId) async { _isLoading = true; + _currentExerciseIndex = 0; notifyListeners(); try { @@ -270,33 +282,93 @@ class ExerciseProvider extends ChangeNotifier { } } + // Di dalam ExerciseProvider class Future fetchReviewExercises(String stdLearningId) async { _isLoading = true; - _isReview = true; + _currentExerciseIndex = 0; notifyListeners(); try { final token = await _userProvider.getValidToken(); - if (token == null) { - throw Exception('No valid token found'); - } + if (token == null) throw Exception('No valid token found'); - final data = await _repository.getStudentAnswers(stdLearningId, token); - _nameTopic = data['NAME_TOPIC']; - _nameLevel = data['NAME_LEVEL']; - final exercisesData = data['stdExercises']; - _exercises = exercisesData - .map((json) => ExerciseModel.fromReviewJson(json)) - .toList(); - _answers = _exercises.map((e) => e.answerStudent ?? '').toList(); + final response = + await _repository.getStudentAnswers(stdLearningId, token); + + // Parse the response data + final payload = response['payload']; + _reviewData = ReviewExerciseModel.fromJson(payload); + + // Sort the exercises based on the title number + _reviewExercises = (_reviewData?.stdExercises ?? []) + ..sort((a, b) { + // Extract numbers from titles (e.g., "Soal 1" -> 1) + int aNumber = int.tryParse(a.title.split(' ').last) ?? 0; + int bNumber = int.tryParse(b.title.split(' ').last) ?? 0; + return aNumber.compareTo(bNumber); + }); + + _nameTopic = _reviewData?.nameTopic ?? ''; + _nameLevel = _reviewData?.nameLevel ?? ''; + + // Reset current index + _currentExerciseIndex = 0; } catch (e) { print('Error fetching review exercises: $e'); + rethrow; } finally { _isLoading = false; notifyListeners(); } } + dynamic get currentExercise { + if (_reviewExercises.isNotEmpty) { + return _reviewExercises[_currentExerciseIndex]; + } else if (_exercises.isNotEmpty) { + return _exercises[_currentExerciseIndex]; + } + return null; + } + + String getAnswerFormatted(ReviewExerciseDetail exercise) { + switch (exercise.questionType) { + case 'MCQ': + if (exercise.multipleChoices != null) { + final choice = exercise.answerStudent; + final choices = exercise.multipleChoices!.first; + switch (choice) { + case 'A': + return choices.optionA; + case 'B': + return choices.optionB; + case 'C': + return choices.optionC; + case 'D': + return choices.optionD; + case 'E': + return choices.optionE; + default: + return exercise.answerStudent; + } + } + return exercise.answerStudent; + case 'TFQ': + return exercise.answerStudent == '1' ? 'True' : 'False'; + case 'MPQ': + if (exercise.answerStudent.isEmpty) return ''; + return exercise.answerStudent.split(', ').map((pair) { + final parts = pair.split('-'); + if (parts.length == 2) { + return '${parts[0]} ➜ ${parts[1]}'; + } + return pair; + }).join('\n'); + default: + return exercise.answerStudent; + } + } + Future> submitAnswersAndGetScore() async { print('submitAnswersAndGetScore called'); try { @@ -398,6 +470,7 @@ class ExerciseProvider extends ChangeNotifier { _nameTopic = previous._nameTopic; _nameLevel = previous._nameLevel; _studentLearningId = previous._studentLearningId; + _reviewData = previous._reviewData; } } } diff --git a/lib/features/learning/modules/exercises/screens/exercise_screen.dart b/lib/features/learning/modules/exercises/screens/exercise_screen.dart index 966db74..be88765 100644 --- a/lib/features/learning/modules/exercises/screens/exercise_screen.dart +++ b/lib/features/learning/modules/exercises/screens/exercise_screen.dart @@ -1,5 +1,6 @@ +import 'package:bootstrap_icons/bootstrap_icons.dart'; 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/content/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'; @@ -31,6 +32,7 @@ class _ExerciseScreenState extends State { _scrollController = ScrollController(); WidgetsBinding.instance.addPostFrameCallback((_) { final provider = context.read(); + if (widget.isReview) { provider.fetchReviewExercises(widget.studentLearningId); } else { @@ -62,11 +64,32 @@ class _ExerciseScreenState extends State { body: Center(child: CircularProgressIndicator()), ); } + + final currentExercise = provider.currentExercise; + final hasExercises = widget.isReview + ? provider.reviewExercises.isNotEmpty + : provider.exercises.isNotEmpty; + + if (!hasExercises || currentExercise == null) { + return const Scaffold( + body: Center(child: Text('No exercises available')), + ); + } + return Scaffold( backgroundColor: AppColors.bgSoftColor, appBar: AppBar( elevation: 0, automaticallyImplyLeading: false, + leading: widget.isReview + ? IconButton( + icon: const Icon( + BootstrapIcons.arrow_left, + color: AppColors.whiteColor, + ), + onPressed: () => Navigator.of(context).pop(), + ) + : null, // Show back button only in review mode iconTheme: const IconThemeData(color: AppColors.whiteColor), centerTitle: true, title: Text( @@ -81,38 +104,47 @@ class _ExerciseScreenState extends State { gradient: AppColors.gradientTheme, ), ), - actions: [ - IconButton( - icon: const Icon(Icons.info_outline, color: AppColors.whiteColor), - onPressed: () => _showInstructions(context), - ), - ], + actions: widget.isReview + ? [ + IconButton( + icon: const Icon( + BootstrapIcons.info_circle, + color: AppColors.whiteColor, + size: 22, + ), + onPressed: () => _showInstructions(context), + ), + ] + : null, ), body: SafeArea( child: Column( children: [ - const Padding( - padding: EdgeInsets.all(16.0), - child: ExerciseProgress(), - ), + if (!widget.isReview) + 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: [ - if (provider.isReview) - Text('Review Mode', style: TextStyle(fontSize: 24)), - const SizedBox(height: 16), - const ExerciseContent(), - const SizedBox(height: 24), - ExerciseNavigator(onScrollToTop: _scrollToTop), - const SizedBox(height: 32), - ], - ), + child: SingleChildScrollView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + const SizedBox(height: 16), + ExerciseContent( + key: ValueKey(provider.currentExerciseIndex), + exercise: currentExercise, + isReview: widget.isReview, + ), + const SizedBox(height: 24), + ExerciseNavigator( + onScrollToTop: _scrollToTop, + isReview: widget.isReview, + ), + const SizedBox(height: 32), + ], ), ), ), diff --git a/lib/features/learning/modules/exercises/widgets/content/exercise_content.dart b/lib/features/learning/modules/exercises/widgets/content/exercise_content.dart new file mode 100644 index 0000000..2cada5f --- /dev/null +++ b/lib/features/learning/modules/exercises/widgets/content/exercise_content.dart @@ -0,0 +1,299 @@ +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:english_learning/core/services/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/models/review_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/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:provider/provider.dart'; + +class ExerciseContent extends StatefulWidget { + final dynamic exercise; + final bool isReview; + + const ExerciseContent({ + super.key, + required this.exercise, + this.isReview = false, + }); + + @override + State createState() => _ExerciseContentState(); +} + +class _ExerciseContentState extends State + with WidgetsBindingObserver { + final GlobalKey _videoPlayerKey = GlobalKey(); + final GlobalKey _audioPlayerKey = GlobalKey(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + _stopAndResetAllMedia(); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + _stopAndResetAllMedia(); + } + } + + void _stopAndResetAllMedia() { + _videoPlayerKey.currentState?.stopAndResetVideo(); + _audioPlayerKey.currentState?.stopAndResetAudio(); + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + final exercises = + widget.isReview ? provider.reviewExercises : provider.exercises; + + if (exercises.isEmpty) { + return const Center(child: Text('No exercises available')); + } + + final currentIndex = provider.currentExerciseIndex; + final currentExercise = exercises[currentIndex]; + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0.05, 0), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + )), + child: child, + ), + ); + }, + child: Container( + key: ValueKey(currentIndex), + 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 ${currentIndex + 1}', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + if (!widget.isReview) + 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, + ), + ), + ), + if (widget.isReview) + _buildStatusIndicator( + currentExercise as ReviewExerciseDetail), + ], + ), + const SizedBox(height: 12), + const Divider( + color: AppColors.disableColor, + thickness: 1, + ), + const SizedBox(height: 12), + Text( + _getQuestionText(currentExercise), + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 16), + if (widget.exercise.image != null && + widget.exercise.image!.isNotEmpty) + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: ImageWidget( + imageFileName: widget.exercise.image!, + baseUrl: '${baseUrl}uploads/exercise/image/', + ), + ), + ), + if (widget.exercise.audio != null && + widget.exercise.audio!.isNotEmpty) + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: AudioPlayerWidget( + key: _audioPlayerKey, + audioFileName: widget.exercise.audio!, + baseUrl: '${baseUrl}uploads/exercise/audio/', + ), + ), + ), + if (widget.exercise.video != null && + widget.exercise.video!.isNotEmpty) + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: VideoPlayerWidget( + key: _videoPlayerKey, + videoUrl: widget.exercise.video!, + ), + ), + ), + const SizedBox(height: 14), + _buildQuestionWidget( + widget.exercise, + widget.isReview, + ) + ], + ), + ), + ), + ); + }); + } + + String _getQuestionText(dynamic exercise) { + if (exercise is ReviewExerciseDetail) { + return exercise.question; + } else if (exercise is ExerciseModel) { + return exercise.question; + } + return ''; + } + + Widget _buildQuestionWidget( + dynamic exercise, + bool isReview, + ) { + String questionType = ''; + + if (exercise is ReviewExerciseDetail) { + questionType = exercise.questionType; + } else if (exercise is ExerciseModel) { + questionType = exercise.questionType; + } + + switch (questionType) { + case 'MCQ': + return MultipleChoiceQuestion( + exercise: exercise, + isReview: isReview, + ); + case 'TFQ': + return TrueFalseQuestion( + exercise: exercise, + isReview: isReview, + ); + case 'MPQ': + return MatchingPairsQuestion( + exercise: exercise, + isReview: isReview, + ); + default: + return const Text('Unsupported question type'); + } + } + + Widget _buildStatusIndicator(ReviewExerciseDetail exercise) { + if (exercise.questionType == 'MPQ') { + return Row( + children: [ + Icon( + exercise.resultScoreStudent > 0 + ? BootstrapIcons.check_circle_fill + : BootstrapIcons.x_circle_fill, + color: exercise.resultScoreStudent > 0 ? Colors.green : Colors.red, + size: 18, + ), + const SizedBox(width: 4), + Text( + '${exercise.resultScoreStudent}/${exercise.scoreWeight}', + style: AppTextStyles.blackTextStyle.copyWith( + color: + exercise.resultScoreStudent > 0 ? Colors.green : Colors.red, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ], + ); + } else { + return Row( + children: [ + Icon( + exercise.isCorrect == 1 + ? BootstrapIcons.check_circle_fill + : BootstrapIcons.x_circle_fill, + color: exercise.isCorrect == 1 ? Colors.green : Colors.red, + size: 18, + ), + const SizedBox(width: 4), + Text( + exercise.isCorrect == 1 ? 'Correct' : 'Incorrect', + style: TextStyle( + color: exercise.isCorrect == 1 ? Colors.green : Colors.red, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ], + ); + } + } +} diff --git a/lib/features/learning/modules/exercises/widgets/exercise_content.dart b/lib/features/learning/modules/exercises/widgets/exercise_content.dart deleted file mode 100644 index 627b849..0000000 --- a/lib/features/learning/modules/exercises/widgets/exercise_content.dart +++ /dev/null @@ -1,149 +0,0 @@ -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 index 0c22a7a..8a604c3 100644 --- a/lib/features/learning/modules/exercises/widgets/exercise_navigator.dart +++ b/lib/features/learning/modules/exercises/widgets/exercise_navigator.dart @@ -1,4 +1,5 @@ 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/learning/modules/exercises/widgets/complete_submission.dart'; import 'package:english_learning/features/learning/modules/exercises/widgets/incomplete_submission.dart'; @@ -9,184 +10,145 @@ import 'package:provider/provider.dart'; class ExerciseNavigator extends StatelessWidget { final VoidCallback? onScrollToTop; + final bool isReview; + const ExerciseNavigator({ super.key, required this.onScrollToTop, + this.isReview = false, }); @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; + builder: (context, exerciseProvider, userProvider, _) { + final currentExerciseIndex = exerciseProvider.currentExerciseIndex; + final totalExercises = isReview + ? exerciseProvider.reviewExercises.length + : exerciseProvider.exercises.length; + final isFirstExercise = currentExerciseIndex == 0; + final isLastExercise = currentExerciseIndex == totalExercises - 1; - Future submitAnswers() async { - try { - final result = await exerciseProvider.submitAnswersAndGetScore(); - print('Submit result: $result'); // Logging + Future submitAnswers() async { + try { + final result = await exerciseProvider.submitAnswersAndGetScore(); + print('Submit result: $result'); - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => ResultScreen( - currentLevel: result['CURRENT_LEVEL_NAME'] ?? '', - nextLevel: result['NEXT_LEARNING_NAME'] ?? '', - score: int.tryParse(result['SCORE'].toString()) ?? 0, - isCompleted: result['IS_PASS'] == 1, - stdLearningId: result['STUDENT_LEARNING_ID']?.toString(), - ), - ), - ); - } catch (e) { - print('Error submitting answers: $e'); // Logging - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: $e')), - ); + if (context.mounted) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => ResultScreen( + currentLevel: result['CURRENT_LEVEL_NAME'] ?? '', + nextLevel: result['NEXT_LEARNING_NAME'] ?? '', + score: int.tryParse(result['SCORE'].toString()) ?? 0, + isCompleted: result['IS_PASS'] == 1, + stdLearningId: result['STUDENT_LEARNING_ID']?.toString(), + ), + ), + ); + } + } catch (e) { + print('Error submitting answers: $e'); + if (context.mounted) { + 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, + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!isFirstExercise) + Expanded( + flex: 3, + child: GlobalButton( + text: 'Previous', + height: 45, + onPressed: () { + exerciseProvider.previousQuestion(); + onScrollToTop?.call(); + }, 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, - ), - ), + borderColor: AppColors.blueColor, + textColor: AppColors.blueColor, + transparentBackground: true, ), ), - ), - if (!isFirstExercise && !isLastExercise) const SizedBox(width: 8), - if (!isLastExercise) - Expanded( - child: ElevatedButton( - onPressed: () { - exerciseProvider.nextQuestion(); - onScrollToTop?.call(); - }, - style: ElevatedButton.styleFrom( - shadowColor: Colors.transparent, + if (!isFirstExercise && !isLastExercise) const SizedBox(width: 8), + if (!isLastExercise) + Expanded( + flex: 3, + child: GlobalButton( + text: 'Next', + height: 45, + onPressed: () { + exerciseProvider.nextQuestion(); + onScrollToTop?.call(); + }, 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, - ), - ), + borderColor: AppColors.blueColor, + textColor: AppColors.blueColor, + transparentBackground: true, ), ), - ), - 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(); - }, - ); - }, - ); + if (isLastExercise && !isFirstExercise) const SizedBox(width: 8), + if (isLastExercise) + Expanded( + flex: 5, + child: GlobalButton( + text: isReview ? 'Back to Level List' : 'Submit', + height: 45, + onPressed: () { + if (isReview) { + Navigator.of(context).pop(); } 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(); + if (exerciseProvider.hasAnsweredQuestions()) { + if (exerciseProvider.hasUnansweredQuestions()) { + showDialog( + context: context, + builder: (BuildContext context) { + return IncompleteSubmission( + onCheckAgain: () => Navigator.of(context).pop(), + onSubmit: () async { + Navigator.of(context).pop(); + submitAnswers(); + }, + ); }, ); - }, - ); + } else { + showDialog( + context: context, + builder: (BuildContext context) { + return CompleteSubmission( + onCheckAgain: () => Navigator.of(context).pop(), + onSubmit: () async { + Navigator.of(context).pop(); + submitAnswers(); + }, + ); + }, + ); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Please answer at least one question before submitting.', + ), + ), + ); + } } - } 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, - ), - ), - ), - ), + }, + gradient: AppColors.gradientTheme, ), ), - ), - ], - ); - }); + ], + ); + }, + ); } } diff --git a/lib/features/learning/modules/exercises/widgets/instruction_dialog.dart b/lib/features/learning/modules/exercises/widgets/instruction_dialog.dart index 6f723eb..cffcd44 100644 --- a/lib/features/learning/modules/exercises/widgets/instruction_dialog.dart +++ b/lib/features/learning/modules/exercises/widgets/instruction_dialog.dart @@ -1,3 +1,5 @@ +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:english_learning/core/widgets/global_button.dart'; import 'package:flutter/material.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; @@ -11,7 +13,6 @@ class InstructionsDialog extends StatelessWidget { elevation: 0, backgroundColor: Colors.transparent, child: Container( - padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.whiteColor, shape: BoxShape.rectangle, @@ -24,47 +25,52 @@ class InstructionsDialog extends StatelessWidget { ), ], ), - 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), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Instructions', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 22, + fontWeight: FontWeight.bold, ), - padding: - const EdgeInsets.symmetric(horizontal: 30, vertical: 10), ), - ), - ], + const SizedBox(height: 16), + const Divider( + thickness: 0.5, + ), + Padding( + padding: const EdgeInsets.only(top: 32, left: 32, right: 32), + child: Column( + children: [ + _buildInstructionItem( + icon: BootstrapIcons.chat_square_text, + text: 'Answer all questions to complete the exercise.', + ), + const SizedBox(height: 12), + _buildInstructionItem( + icon: BootstrapIcons.arrow_left_right, + text: + 'For matching pairs, select a left option first, then match it with a right option.', + ), + const SizedBox(height: 12), + _buildInstructionItem( + icon: BootstrapIcons.pencil, + text: + 'You can change your answers at any time before submitting.', + ), + const SizedBox(height: 32), + GlobalButton( + text: 'Got It', + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + ], + ), ), ), ); @@ -73,12 +79,16 @@ class InstructionsDialog extends StatelessWidget { Widget _buildInstructionItem({required IconData icon, required String text}) { return Row( children: [ - Icon(icon, color: AppColors.blueColor), - const SizedBox(width: 12), + Icon( + icon, + color: AppColors.blueColor, + size: 24, + ), + const SizedBox(width: 16), Expanded( child: Text( text, - style: AppTextStyles.blackTextStyle.copyWith(fontSize: 16), + style: AppTextStyles.greyTextStyle.copyWith(fontSize: 14), ), ), ], 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 index f8fcc90..a0e53f9 100644 --- a/lib/features/learning/modules/exercises/widgets/question/matching_pairs_question.dart +++ b/lib/features/learning/modules/exercises/widgets/question/matching_pairs_question.dart @@ -1,3 +1,4 @@ +import 'package:english_learning/features/learning/modules/exercises/models/review_exercise_model.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'; @@ -5,55 +6,131 @@ import 'package:english_learning/features/learning/modules/exercises/models/exer import 'package:provider/provider.dart'; class MatchingPairsQuestion extends StatelessWidget { - final ExerciseModel exercise; + final dynamic exercise; + final bool isReview; const MatchingPairsQuestion({ super.key, required this.exercise, + this.isReview = false, }); @override Widget build(BuildContext context) { final provider = Provider.of(context); - final matchingPair = exercise.choices as MatchingPair; + List pairs = []; + Map studentAnswers = {}; final currentIndex = provider.currentExerciseIndex; + if (exercise is ExerciseModel) { + pairs = (exercise.choices as MatchingPair).pairs; + } else if (exercise is ReviewExerciseDetail) { + if (exercise.matchingPairs != null) { + pairs = exercise.matchingPairs!.map((reviewPair) { + return Pair( + left: reviewPair.leftPair, + right: reviewPair.rightPair, + ); + }).toList(); + + // Parse student answers + if (exercise.answerStudent.isNotEmpty) { + final answerPairs = exercise.answerStudent.split(', '); + for (var pair in answerPairs) { + final parts = pair.split('-'); + if (parts.length == 2) { + studentAnswers[parts[0]] = parts[1]; + } + } + } + } + } + + // Create a reverse mapping for easier lookup of left pair from right pair + Map reverseStudentAnswers = {}; + studentAnswers.forEach((left, right) { + reverseStudentAnswers[right] = left; + }); + return Column( children: [ - ...matchingPair.pairs.asMap().entries.map((entry) { + ...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, + dynamic pair = entry.value; + String left = pair is Pair ? pair.left : pair.leftPair; + String right = pair is Pair ? pair.right : pair.rightPair; + + // Get the corresponding color for this pair + Color pairColor = _getPairColor(pairIndex); + + // Find if this item is matched in student answers + String? studentMatchedRight = studentAnswers[left]; + String? matchedLeftForRight = reverseStudentAnswers[right]; + + // Find the color index for the matched pair + int? matchedPairIndex; + if (isReview && matchedLeftForRight != null) { + matchedPairIndex = pairs.indexWhere((p) { + if (p is Pair) { + return p.left == matchedLeftForRight; + } + return false; + }); + } + + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: _buildOptionItem( + context, + left, + provider, + currentIndex, + pairIndex, + true, + studentMatchedRight != null, + studentAnswers, + pairColor, + ), ), - ), - const SizedBox(width: 10), - Expanded( - child: _buildOptionItem( - context, - pair.right, - provider, - currentIndex, - pairIndex, - false, + const SizedBox(width: 10), + Expanded( + child: _buildOptionItem( + context, + right, + provider, + currentIndex, + matchedPairIndex ?? pairIndex, + false, + matchedLeftForRight != null, + studentAnswers, + matchedPairIndex != null + ? _getPairColor(matchedPairIndex) + : pairColor, + ), ), - ), - ], + ], + ), ); }).toList(), ], ); } + Color _getPairColor(int index) { + final List colors = [ + Colors.blue, + Colors.green, + Colors.orange, + Colors.purple, + Colors.red, + ]; + return colors[index % colors.length]; + } + Widget _buildOptionItem( BuildContext context, String option, @@ -61,61 +138,79 @@ class MatchingPairsQuestion extends StatelessWidget { int exerciseIndex, int pairIndex, bool isLeft, + bool isMatched, + Map studentAnswers, + Color pairColor, ) { - Color? color = isLeft - ? provider.getLeftColor(exerciseIndex, pairIndex) - : provider.getRightColor(exerciseIndex, pairIndex); - final isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft); - final isActive = isLeft && provider.activeLeftOption == option; + Color? color; + bool isSelected = false; + bool isActive = false; + + if (isReview) { + if (isMatched) { + color = pairColor; + isSelected = true; + } + } else { + if (isLeft) { + color = provider.getLeftColor(exerciseIndex, pairIndex); + isActive = provider.activeLeftOption == option; + } else { + color = provider.getRightColor(exerciseIndex, pairIndex); + } + isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft); + } 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, + onTap: isReview + ? null + : () { + if (!isLeft && provider.activeLeftOption == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please select a left option first.'), + duration: Duration(seconds: 1), + ), + ); + } else { + provider.answerQuestion(exerciseIndex, option); + } + }, + child: Container( + decoration: BoxDecoration( + color: isSelected + ? color ?? pairColor + : (isActive ? pairColor : AppColors.whiteColor), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(20), + bottomRight: Radius.circular(20), + bottomLeft: Radius.circular(20), ), - 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, - ), + border: Border.all( + color: isSelected + ? color ?? AppColors.cardDisabledColor + : AppColors.cardDisabledColor, + width: isSelected ? 2 : 1, + ), + boxShadow: isActive && !isReview + ? [ + BoxShadow( + color: pairColor.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 index 88e0725..3abcf5c 100644 --- a/lib/features/learning/modules/exercises/widgets/question/multiple_choice_question.dart +++ b/lib/features/learning/modules/exercises/widgets/question/multiple_choice_question.dart @@ -1,3 +1,4 @@ +import 'package:english_learning/features/learning/modules/exercises/models/review_exercise_model.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'; @@ -5,54 +6,85 @@ import 'package:english_learning/features/learning/modules/exercises/models/exer import 'package:provider/provider.dart'; class MultipleChoiceQuestion extends StatelessWidget { - final ExerciseModel exercise; + final dynamic exercise; + final bool isReview; const MultipleChoiceQuestion({ super.key, required this.exercise, + this.isReview = false, }); @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, - ]; + List options = []; + String? studentAnswer; - return _buildOptionsList(options, provider); + if (exercise is ExerciseModel) { + final multipleChoice = exercise.choices as MultipleChoice; + options = [ + multipleChoice.optionA, + multipleChoice.optionB, + multipleChoice.optionC, + multipleChoice.optionD, + multipleChoice.optionE, + ]; + } else if (exercise is ReviewExerciseDetail) { + if (exercise.multipleChoices?.isNotEmpty ?? false) { + final choices = exercise.multipleChoices!.first; + options = [ + choices.optionA, + choices.optionB, + choices.optionC, + choices.optionD, + choices.optionE, + ]; + studentAnswer = exercise.answerStudent; + } + } + + return _buildOptionsList(context, options, provider, studentAnswer); } - Widget _buildOptionsList(List options, ExerciseProvider provider) { - final optionLabels = List.generate( - options.length, - (index) => - String.fromCharCode(65 + index), // Generate labels "A", "B", etc. - ); + Widget _buildOptionsList( + BuildContext context, + List options, + ExerciseProvider provider, + String? studentAnswer, + ) { + return Column( + children: options.asMap().entries.map((entry) { + final index = entry.key; + final option = entry.value; + final optionLabel = String.fromCharCode(65 + index); // A, B, C, D, E - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: options.length, - itemBuilder: (context, i) { - final option = options[i]; - final isSelected = - provider.answers[provider.currentExerciseIndex] == optionLabels[i]; + bool isSelected = false; + if (isReview) { + isSelected = studentAnswer == optionLabel; + } else { + isSelected = + provider.answers[provider.currentExerciseIndex] == optionLabel; + } return GestureDetector( - onTap: () => - provider.answerQuestion(provider.currentExerciseIndex, option), - child: _buildOptionItem(optionLabels[i], option, isSelected), + onTap: isReview + ? null + : () { + provider.answerQuestion( + provider.currentExerciseIndex, option); + }, + child: _buildOptionItem(optionLabel, option, isSelected), ); - }, + }).toList(), ); } Widget _buildOptionItem(String label, String option, bool isSelected) { + final backgroundColor = + isSelected ? AppColors.blueColor : AppColors.whiteColor; + final textColor = isSelected ? AppColors.whiteColor : AppColors.blackColor; + return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Row( @@ -60,7 +92,7 @@ class MultipleChoiceQuestion extends StatelessWidget { children: [ Container( decoration: BoxDecoration( - color: isSelected ? AppColors.blueColor : AppColors.whiteColor, + color: backgroundColor, borderRadius: BorderRadius.circular(25), border: Border.all( color: isSelected @@ -68,16 +100,13 @@ class MultipleChoiceQuestion extends StatelessWidget { : 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, - ), + padding: + const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0), + child: Text( + label, + style: AppTextStyles.blackTextStyle.copyWith( + fontWeight: FontWeight.w900, + color: textColor, ), ), ), @@ -85,9 +114,8 @@ class MultipleChoiceQuestion extends StatelessWidget { Expanded( child: Container( margin: const EdgeInsets.only(bottom: 8.0), - width: double.infinity, decoration: BoxDecoration( - color: isSelected ? AppColors.blueColor : AppColors.whiteColor, + color: backgroundColor, borderRadius: const BorderRadius.only( topRight: Radius.circular(20), bottomRight: Radius.circular(20), @@ -104,8 +132,7 @@ class MultipleChoiceQuestion extends StatelessWidget { child: Text( option, style: AppTextStyles.blackTextStyle.copyWith( - color: - isSelected ? AppColors.whiteColor : AppColors.blackColor, + color: textColor, ), ), ), 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 index 34bbba4..d8afb03 100644 --- a/lib/features/learning/modules/exercises/widgets/question/true_false_question.dart +++ b/lib/features/learning/modules/exercises/widgets/question/true_false_question.dart @@ -1,49 +1,90 @@ -// 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'; +import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/learning/modules/exercises/models/review_exercise_model.dart'; +import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; class TrueFalseQuestion extends StatelessWidget { - final ExerciseModel exercise; + final dynamic exercise; + final bool isReview; const TrueFalseQuestion({ super.key, required this.exercise, + this.isReview = false, }); @override Widget build(BuildContext context) { final provider = Provider.of(context); - final options = ['True', 'False']; + String? studentAnswer = _getStudentAnswer(); - return _buildOptionsList(options, provider); + final options = [ + {'label': 'A', 'value': '1', 'text': 'True'}, + {'label': 'B', 'value': '0', 'text': 'False'} + ]; + + return _buildOptionsList(context, options, provider, studentAnswer); } - 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] == - (i == 0 ? '1' : '0'); + String? _getStudentAnswer() { + if (isReview && exercise is ReviewExerciseDetail) { + return exercise.answerStudent; + } + return null; + } + + Widget _buildOptionsList( + BuildContext context, + List> options, + ExerciseProvider provider, + String? studentAnswer, + ) { + return Column( + children: options.map((option) { + final value = option['value']!; + final label = option['label']!; + final text = option['text']!; + + bool isSelected = false; + bool isCorrect = false; + + if (isReview && exercise is ReviewExerciseDetail) { + isSelected = studentAnswer == value; + isCorrect = exercise.isCorrect == 1; + } else { + isSelected = provider.answers[provider.currentExerciseIndex] == value; + } return GestureDetector( - // onTap: () => - // provider.answerQuestion(provider.currentExerciseIndex, option), - onTap: () => provider.answerQuestion( - provider.currentExerciseIndex, i == 0 ? '1' : '0'), - - child: _buildOptionItem(option, isSelected), + onTap: isReview + ? null + : () { + provider.answerQuestion(provider.currentExerciseIndex, value); + }, + child: _buildOptionItem( + label, + text, + isSelected, + isReview && isSelected, + isCorrect, + ), ); - }, + }).toList(), ); } - Widget _buildOptionItem(String option, bool isSelected) { + Widget _buildOptionItem( + String label, + String text, + bool isSelected, + bool isReviewSelected, + bool isCorrect, + ) { + final backgroundColor = + isSelected ? AppColors.blueColor : AppColors.whiteColor; + final textColor = isSelected ? AppColors.whiteColor : AppColors.blackColor; + return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Row( @@ -51,7 +92,7 @@ class TrueFalseQuestion extends StatelessWidget { children: [ Container( decoration: BoxDecoration( - color: isSelected ? AppColors.blueColor : AppColors.whiteColor, + color: backgroundColor, borderRadius: BorderRadius.circular(25), border: Border.all( color: isSelected @@ -59,18 +100,13 @@ class TrueFalseQuestion extends StatelessWidget { : 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, - ), + padding: + const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0), + child: Text( + label, + style: AppTextStyles.blackTextStyle.copyWith( + fontWeight: FontWeight.w900, + color: textColor, ), ), ), @@ -78,9 +114,8 @@ class TrueFalseQuestion extends StatelessWidget { Expanded( child: Container( margin: const EdgeInsets.only(bottom: 8.0), - width: double.infinity, decoration: BoxDecoration( - color: isSelected ? AppColors.blueColor : AppColors.whiteColor, + color: backgroundColor, borderRadius: const BorderRadius.only( topRight: Radius.circular(20), bottomRight: Radius.circular(20), @@ -95,10 +130,9 @@ class TrueFalseQuestion extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), child: Text( - option, + text, style: AppTextStyles.blackTextStyle.copyWith( - color: - isSelected ? AppColors.whiteColor : AppColors.blackColor, + color: textColor, ), ), ), diff --git a/lib/features/learning/modules/level/providers/level_provider.dart b/lib/features/learning/modules/level/providers/level_provider.dart index 8014d65..af5730b 100644 --- a/lib/features/learning/modules/level/providers/level_provider.dart +++ b/lib/features/learning/modules/level/providers/level_provider.dart @@ -71,22 +71,6 @@ class LevelProvider with ChangeNotifier { // _lastCompletedLevel!['ID_LEVEL'] == levelId; // } - Future fetchStudentAnswers(String stdLearningId, String token) async { - _isLoading = true; - _error = null; - notifyListeners(); - - try { - _studentAnswers = - await _levelRepository.getStudentAnswers(stdLearningId, token); - } catch (e) { - _error = 'Error fetching student answers: ${e.toString()}'; - } finally { - _isLoading = false; - notifyListeners(); - } - } - bool isPretestFinished(String levelId) { return _levels.any( (level) => level.idLevel == levelId && level.idStudentLearning != null); diff --git a/lib/features/learning/modules/level/widgets/pretest_card.dart b/lib/features/learning/modules/level/widgets/pretest_card.dart index 1bb8112..1b4dc8d 100644 --- a/lib/features/learning/modules/level/widgets/pretest_card.dart +++ b/lib/features/learning/modules/level/widgets/pretest_card.dart @@ -27,6 +27,32 @@ class PretestCard extends StatelessWidget { final isFinished = levelProvider.isPretestFinished(pretest.idLevel); final score = levelProvider.getPretestScore(pretest.idLevel); // final isAllowed = levelProvider.isLevelAllowed(pretest.idLevel); + void navigateToMaterial() { + if (isFinished) { + // Mode review untuk pretest yang sudah selesai + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MaterialScreen( + levelId: pretest.idLevel, + isReview: true, + studentLearningId: pretest.idStudentLearning, + ), + ), + ); + } else { + // Mode normal untuk pretest yang belum dikerjakan + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MaterialScreen( + levelId: pretest.idLevel, + isReview: false, + ), + ), + ); + } + } return Card( shape: RoundedRectangleBorder( @@ -140,17 +166,7 @@ class PretestCard extends StatelessWidget { // ); // } // : () {}, - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MaterialScreen( - levelId: pretest.idLevel, - isReview: isFinished, - ), - ), - ); - }, + onPressed: navigateToMaterial, ), ], ), diff --git a/lib/features/learning/modules/material/screens/material_screen.dart b/lib/features/learning/modules/material/screens/material_screen.dart index 2ebe2c2..2ad8f65 100644 --- a/lib/features/learning/modules/material/screens/material_screen.dart +++ b/lib/features/learning/modules/material/screens/material_screen.dart @@ -1,5 +1,5 @@ import 'package:english_learning/core/services/dio_client.dart'; -import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/core/services/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'; @@ -15,11 +15,13 @@ import 'package:provider/provider.dart'; class MaterialScreen extends StatefulWidget { final String levelId; final bool isReview; + final String? studentLearningId; const MaterialScreen({ super.key, required this.levelId, this.isReview = false, + this.studentLearningId, }); @override @@ -59,6 +61,11 @@ class _MaterialScreenState extends State } Future _createStudentLearning() async { + if (widget.isReview && widget.studentLearningId != null) { + // Jika mode review dan studentLearningId tersedia, langsung navigasi ke ExerciseScreen + _navigateToExercise(widget.studentLearningId!); + return; + } setState(() { _isLoading = true; }); @@ -77,15 +84,7 @@ class _MaterialScreenState extends State print('Student Learning created: ${result['message']}'); // Navigate to ExerciseScreen - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => ExerciseScreen( - levelId: widget.levelId, - studentLearningId: result['payload']['ID_STUDENT_LEARNING'], - ), - ), - ); + _navigateToExercise(result['payload']['ID_STUDENT_LEARNING']); } catch (e) { // Show error message ScaffoldMessenger.of(context).showSnackBar( @@ -98,6 +97,19 @@ class _MaterialScreenState extends State } } + void _navigateToExercise(String studentLearningId) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => ExerciseScreen( + levelId: widget.levelId, + studentLearningId: studentLearningId, + isReview: widget.isReview, + ), + ), + ); + } + @override Widget build(BuildContext context) { return Consumer(builder: (context, levelProvider, child) { diff --git a/lib/features/learning/modules/material/widgets/video_player_widget.dart b/lib/features/learning/modules/material/widgets/video_player_widget.dart index 290ed05..4bbcf5e 100644 --- a/lib/features/learning/modules/material/widgets/video_player_widget.dart +++ b/lib/features/learning/modules/material/widgets/video_player_widget.dart @@ -16,21 +16,48 @@ class VideoPlayerWidget extends StatefulWidget { } class VideoPlayerWidgetState extends State { - late Widget _videoWidget; VideoPlayerController? _videoController; YoutubePlayerController? _youtubeController; FlickManager? _flickManager; bool _isLoading = true; String? _error; - - bool get wantKeepAlive => true; + bool _isYoutubeReady = false; + String? _youtubeId; @override void initState() { super.initState(); + _youtubeId = _extractYoutubeId(widget.videoUrl); _initializeVideoPlayerWidget(); } + String? _extractYoutubeId(String url) { + try { + RegExp regExp = RegExp( + r'^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/|shorts\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*', + caseSensitive: false, + multiLine: false, + ); + + String? videoId = YoutubePlayer.convertUrlToId(url); + if (videoId != null) return videoId; + + Match? match = regExp.firstMatch(url); + if (match != null && match.groupCount >= 1) { + return match.group(1); + } + + if (url.contains('youtu.be/')) { + return url.split('youtu.be/')[1].split(RegExp(r'[?&]'))[0]; + } + + return null; + } catch (e) { + print('Error extracting YouTube ID: $e'); + return null; + } + } + String _getPlayableUrl(String url) { if (url.contains('drive.google.com')) { final regex = RegExp(r'/d/([a-zA-Z0-9-_]+)'); @@ -43,54 +70,66 @@ class VideoPlayerWidgetState extends State { return url; } - void _initializeVideoPlayerWidget() { - if (YoutubePlayer.convertUrlToId(widget.videoUrl) != null) { - _youtubeController = YoutubePlayerController( - initialVideoId: YoutubePlayer.convertUrlToId(widget.videoUrl)!, - flags: const YoutubePlayerFlags( - autoPlay: false, - mute: false, - ), - ); + Future _initializeVideoPlayerWidget() async { + if (_youtubeId != null) { + try { + _youtubeController = YoutubePlayerController( + initialVideoId: _youtubeId!, + flags: const YoutubePlayerFlags( + autoPlay: false, + mute: false, + hideControls: false, + controlsVisibleAtStart: true, + enableCaption: true, + useHybridComposition: true, + forceHD: true, + ), + ); - _videoWidget = YoutubePlayer( - controller: _youtubeController!, - showVideoProgressIndicator: true, - onReady: () { - _youtubeController!.addListener(_youtubeListener); + if (mounted) { + setState(() {}); // Trigger rebuild with controller + } + } catch (e) { + if (mounted) { setState(() { + _error = "Error initializing YouTube player: $e"; _isLoading = false; }); - }, - ); + } + } } else { - _videoController = VideoPlayerController.networkUrl( - Uri.parse(_getPlayableUrl(widget.videoUrl)), - ); - _videoController!.initialize().then((_) { + try { + _videoController = VideoPlayerController.networkUrl( + Uri.parse(_getPlayableUrl(widget.videoUrl)), + ); + + await _videoController!.initialize(); + _flickManager = FlickManager( videoPlayerController: _videoController!, autoPlay: false, ); - _videoWidget = FlickVideoPlayer( - flickManager: _flickManager!, - ); - setState(() { - _isLoading = false; - }); - }).catchError((error) { - setState(() { - _error = "Error loading video: $error"; - _isLoading = false; - }); - }); + + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _error = "Error initializing video player: $e"; + _isLoading = false; + }); + } + } } } void _youtubeListener() { - if (_youtubeController!.value.playerState == PlayerState.ended) { - _youtubeController!.seekTo(Duration.zero); - _youtubeController!.pause(); + if (_youtubeController?.value.playerState == PlayerState.ended) { + _youtubeController?.seekTo(Duration.zero); + _youtubeController?.pause(); } } @@ -104,20 +143,105 @@ class VideoPlayerWidgetState extends State { @override Widget build(BuildContext context) { - if (_isLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (_error != null) { - return Center(child: Text(_error!)); + if (_youtubeId != null && _youtubeController != null) { + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: AspectRatio( + aspectRatio: 16 / 9, + child: YoutubePlayerBuilder( + player: YoutubePlayer( + controller: _youtubeController!, + showVideoProgressIndicator: true, + progressIndicatorColor: Colors.red, + progressColors: const ProgressBarColors( + playedColor: Colors.red, + handleColor: Colors.redAccent, + ), + onReady: () { + if (mounted) { + setState(() { + _isYoutubeReady = true; + _isLoading = false; + }); + } + _youtubeController!.addListener(_youtubeListener); + }, + onEnded: (YoutubeMetaData metaData) { + _youtubeController!.seekTo(Duration.zero); + _youtubeController!.pause(); + }, + bottomActions: [ + CurrentPosition(), + ProgressBar(isExpanded: true), + RemainingDuration(), + PlaybackSpeedButton(), + ], + ), + builder: (context, player) { + return Container( + color: Colors.black, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: _isYoutubeReady + ? player + : const Center( + child: CircularProgressIndicator( + valueColor: + AlwaysStoppedAnimation(Colors.white), + ), + ), + ), + ); + }, + ), + ), + ); } + return ClipRRect( borderRadius: BorderRadius.circular(16), child: AspectRatio( aspectRatio: 16 / 9, - child: _videoWidget, + child: Container( + color: Colors.black, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: _buildContent(), + ), + ), ), ); } + Widget _buildContent() { + if (_error != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + _error!, + style: const TextStyle(color: Colors.white), + textAlign: TextAlign.center, + ), + ), + ); + } + + if (_isLoading) { + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ); + } + + if (_flickManager != null) { + return FlickVideoPlayer(flickManager: _flickManager!); + } + + return const SizedBox.shrink(); + } + void stopAndResetVideo() { if (_youtubeController != null) { _youtubeController!.seekTo(Duration.zero); diff --git a/lib/features/learning/modules/topics/screens/topics_list_screen.dart b/lib/features/learning/modules/topics/screens/topics_list_screen.dart index 7fbe182..bc332a0 100644 --- a/lib/features/learning/modules/topics/screens/topics_list_screen.dart +++ b/lib/features/learning/modules/topics/screens/topics_list_screen.dart @@ -1,4 +1,4 @@ -import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/core/services/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'; diff --git a/lib/features/learning/screens/learning_screen.dart b/lib/features/learning/screens/learning_screen.dart index 3910a77..5b66e46 100644 --- a/lib/features/learning/screens/learning_screen.dart +++ b/lib/features/learning/screens/learning_screen.dart @@ -2,10 +2,10 @@ 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:english_learning/features/learning/widgets/section_card_shimmer.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({ @@ -102,9 +102,7 @@ class _LearningScreenState extends State { 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]!, + return ShimmerWidget( child: Container( margin: const EdgeInsets.only(bottom: 16), child: Row( diff --git a/lib/features/learning/widgets/section_card.dart b/lib/features/learning/widgets/section_card.dart index dd8c6dc..e3c4502 100644 --- a/lib/features/learning/widgets/section_card.dart +++ b/lib/features/learning/widgets/section_card.dart @@ -1,9 +1,9 @@ -import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/core/services/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 { +class LearningCard extends StatefulWidget { final Section section; final VoidCallback? onTap; @@ -13,6 +13,12 @@ class LearningCard extends StatelessWidget { this.onTap, }); + @override + State createState() => _LearningCardState(); +} + +class _LearningCardState extends State + with SingleTickerProviderStateMixin { String _getFullImageUrl(String thumbnail) { if (thumbnail.startsWith('http')) { return thumbnail; @@ -24,7 +30,7 @@ class LearningCard extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: onTap, + onTap: widget.onTap, child: Card( color: AppColors.whiteColor, shape: RoundedRectangleBorder( @@ -40,7 +46,7 @@ class LearningCard extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( - _getFullImageUrl(section.thumbnail), + _getFullImageUrl(widget.section.thumbnail), width: 90, height: 104, fit: BoxFit.cover, @@ -72,7 +78,7 @@ class LearningCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - section.name, + widget.section.name, style: AppTextStyles.blackTextStyle.copyWith( fontSize: 16, fontWeight: FontWeight.w900, @@ -80,7 +86,7 @@ class LearningCard extends StatelessWidget { ), const SizedBox(height: 4), Text( - section.description, + widget.section.description, style: AppTextStyles.disableTextStyle.copyWith( fontSize: 13, fontWeight: FontWeight.w500, diff --git a/lib/features/learning/widgets/section_card_shimmer.dart b/lib/features/learning/widgets/section_card_shimmer.dart index 0cfe126..70c81f6 100644 --- a/lib/features/learning/widgets/section_card_shimmer.dart +++ b/lib/features/learning/widgets/section_card_shimmer.dart @@ -3,6 +3,7 @@ import 'package:shimmer/shimmer.dart'; class ShimmerWidget extends StatelessWidget { final Widget child; + const ShimmerWidget({ super.key, required this.child, 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 index 5801546..9579667 100644 --- a/lib/features/settings/modules/edit_profile/screens/edit_profile_screen.dart +++ b/lib/features/settings/modules/edit_profile/screens/edit_profile_screen.dart @@ -1,5 +1,5 @@ import 'dart:io'; -import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/core/services/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'; @@ -60,7 +60,9 @@ class _EditProfileScreenState extends State { }; try { + userProvider.setLoading(true); await userProvider.updateUserProfile(updatedData); + userProvider.setLoading(false); showDialog( context: context, @@ -186,7 +188,10 @@ class _EditProfileScreenState extends State { const SizedBox(height: 24), GlobalButton( text: 'Save Changes', - onPressed: () => _updateUserProfile(context), + isLoading: userProvider.isLoading, + onPressed: userProvider.isLoading + ? null + : () => _updateUserProfile(context), ), ], ), diff --git a/lib/features/settings/screens/settings_screen.dart b/lib/features/settings/screens/settings_screen.dart index a7f225d..23a7dd0 100644 --- a/lib/features/settings/screens/settings_screen.dart +++ b/lib/features/settings/screens/settings_screen.dart @@ -1,5 +1,5 @@ import 'package:bootstrap_icons/bootstrap_icons.dart'; -import 'package:english_learning/core/services/repositories/constants.dart'; +import 'package:english_learning/core/services/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'; diff --git a/pubspec.lock b/pubspec.lock index abc9aa0..7206f65 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -676,10 +676,10 @@ packages: dependency: transitive description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: