feat: implement pretest review feature and media player optimization

This commit is contained in:
Naresh Pratista 2024-10-31 16:03:22 +07:00
parent 210c40812b
commit 64454f596e
35 changed files with 1606 additions and 875 deletions

View File

@ -0,0 +1,2 @@
const String baseUrl =
'https://3be6-2001-448a-50a0-58e3-1c9f-405a-b360-22ed.ngrok-free.app/';

View File

@ -1,7 +1,7 @@
// ignore_for_file: avoid_print // ignore_for_file: avoid_print
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:english_learning/core/services/repositories/constants.dart'; import 'package:english_learning/core/services/constants.dart';
class DioClient { class DioClient {
final Dio _dio = Dio(); final Dio _dio = Dio();
@ -393,7 +393,7 @@ class DioClient {
Future<Response> getStudentAnswers(String stdLearningId, String token) async { Future<Response> getStudentAnswers(String stdLearningId, String token) async {
try { try {
final response = await _dio.get( final response = await _dio.get(
'/studentAnswer/$stdLearningId', '/studentAnswers/$stdLearningId',
options: Options( options: Options(
headers: { headers: {
'Authorization': 'Bearer $token', 'Authorization': 'Bearer $token',

View File

@ -1,2 +0,0 @@
const String baseUrl =
'https://70e7-2001-448a-50a0-2604-b558-2a22-54f6-65ce.ngrok-free.app/';

View File

@ -25,7 +25,7 @@ class ExerciseRepository {
try { try {
final response = await _dioClient.getStudentAnswers(stdLearningId, token); final response = await _dioClient.getStudentAnswers(stdLearningId, token);
if (response.statusCode == 200) { if (response.statusCode == 200) {
return response.data['data']; return response.data;
} else { } else {
throw Exception('Failed to load student answers'); throw Exception('Failed to load student answers');
} }

View File

@ -32,20 +32,4 @@ class LevelRepository {
rethrow; rethrow;
} }
} }
Future<Map<String, dynamic>> 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');
}
}
} }

View File

@ -15,6 +15,7 @@ class GlobalButton extends StatelessWidget {
final Color? borderColor; final Color? borderColor;
final double borderWidth; final double borderWidth;
final bool transparentBackground; final bool transparentBackground;
final bool isLoading;
const GlobalButton({ const GlobalButton({
super.key, super.key,
@ -31,6 +32,7 @@ class GlobalButton extends StatelessWidget {
this.borderColor, this.borderColor,
this.borderWidth = 1.0, this.borderWidth = 1.0,
this.transparentBackground = false, this.transparentBackground = false,
this.isLoading = false,
}); });
@override @override
@ -61,8 +63,26 @@ class GlobalButton extends StatelessWidget {
), ),
), ),
onPressed: onPressed, onPressed: onPressed,
child: spaceBetween && icon != null child: isLoading ? _buildLoadingIndicator() : _buildButtonContent(),
? Row( ),
),
);
}
Widget _buildLoadingIndicator() {
return SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(textColor ?? Colors.white),
strokeWidth: 2.0,
),
);
}
Widget _buildButtonContent() {
if (spaceBetween && icon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
@ -79,8 +99,9 @@ class GlobalButton extends StatelessWidget {
color: textColor ?? Colors.white, color: textColor ?? Colors.white,
), ),
], ],
) );
: Row( } else {
return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
@ -101,9 +122,7 @@ class GlobalButton extends StatelessWidget {
), ),
), ),
], ],
),
),
),
); );
} }
} }
}

View File

@ -9,7 +9,9 @@ class UserProvider with ChangeNotifier {
String? _jwtToken; String? _jwtToken;
Map<String, dynamic>? _userData; Map<String, dynamic>? _userData;
File? _selectedImage; File? _selectedImage;
bool _isLoading = false;
bool get isLoading => _isLoading;
bool get isLoggedIn => _isLoggedIn; bool get isLoggedIn => _isLoggedIn;
String? get jwtToken => _jwtToken; String? get jwtToken => _jwtToken;
Map<String, dynamic>? get userData => _userData; Map<String, dynamic>? get userData => _userData;
@ -19,6 +21,11 @@ class UserProvider with ChangeNotifier {
_loadLoginStatus(); _loadLoginStatus();
} }
void setLoading(bool value) {
_isLoading = value;
notifyListeners();
}
Future<void> _loadLoginStatus() async { Future<void> _loadLoginStatus() async {
try { try {
_jwtToken = await _userRepository.getToken(); _jwtToken = await _userRepository.getToken();
@ -60,6 +67,7 @@ class UserProvider with ChangeNotifier {
} }
Future<bool> login({required String email, required String password}) async { Future<bool> login({required String email, required String password}) async {
setLoading(true);
try { try {
final response = await _userRepository.loginUser({ final response = await _userRepository.loginUser({
'EMAIL': email, 'EMAIL': email,
@ -84,6 +92,8 @@ class UserProvider with ChangeNotifier {
} catch (e) { } catch (e) {
print('Login error: $e'); print('Login error: $e');
return false; return false;
} finally {
setLoading(false);
} }
} }

View File

@ -128,7 +128,10 @@ class SigninScreen extends StatelessWidget {
const SizedBox(height: 24), const SizedBox(height: 24),
GlobalButton( GlobalButton(
text: 'Login', text: 'Login',
onPressed: () async { isLoading: userProvider.isLoading,
onPressed: userProvider.isLoading
? null
: () async {
// Validate email and password fields // Validate email and password fields
validatorProvider.validateField( validatorProvider.validateField(
'email', 'email',
@ -143,7 +146,8 @@ class SigninScreen extends StatelessWidget {
// If no errors, proceed with login // If no errors, proceed with login
if (validatorProvider.getError('email') == null && if (validatorProvider.getError('email') == null &&
validatorProvider.getError('password') == null) { validatorProvider.getError('password') ==
null) {
final isSuccess = await userProvider.login( final isSuccess = await userProvider.login(
email: _emailController.text, email: _emailController.text,
password: _passwordController.text, password: _passwordController.text,
@ -154,7 +158,8 @@ class SigninScreen extends StatelessWidget {
Navigator.pushAndRemoveUntil( Navigator.pushAndRemoveUntil(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const HomeScreen()), builder: (context) =>
const HomeScreen()),
(Route<dynamic> route) => (Route<dynamic> route) =>
false, // Remove all previous routes false, // Remove all previous routes
).then((_) { ).then((_) {
@ -187,6 +192,7 @@ class SigninScreen extends StatelessWidget {
), ),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
context.read<ValidatorProvider>().resetFields();
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(

View File

@ -227,13 +227,12 @@ class SignupScreen extends StatelessWidget {
), ),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
context.read<ValidatorProvider>().resetFields();
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => SigninScreen()), builder: (context) => SigninScreen()),
).then((_) { );
context.read<ValidatorProvider>().resetFields();
});
}, },
child: Text( child: Text(
' Login Here', ' Login Here',

View File

@ -51,6 +51,7 @@ class HistoryProvider with ChangeNotifier {
_error = null; _error = null;
} catch (e) { } catch (e) {
_error = 'Error fetching learning history: ${e.toString()}'; _error = 'Error fetching learning history: ${e.toString()}';
_learningHistory = [];
} finally { } finally {
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
@ -91,10 +92,23 @@ class HistoryProvider with ChangeNotifier {
} }
Future<void> refreshData(String token) async { Future<void> refreshData(String token) async {
if (_sectionProvider.sections.isEmpty) { _isLoading = true;
_error = null;
notifyListeners();
try {
await _sectionProvider.fetchSections(token); await _sectionProvider.fetchSections(token);
} if (_sectionProvider.sections.isNotEmpty) {
await fetchLearningHistory(token); await fetchLearningHistory(token);
} else {
_error = 'No sections available';
}
} catch (e) {
_error = 'Error refreshing data: ${e.toString()}';
} finally {
_isLoading = false;
notifyListeners();
}
} }
Future<void> loadHistoryData(String token) async { Future<void> loadHistoryData(String token) async {
@ -103,29 +117,52 @@ class HistoryProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
try { try {
// Fetch sections and learning history in parallel await _sectionProvider.fetchSections(token);
final sectionsResult = _sectionProvider.fetchSections(token); if (_sectionProvider.sections.isNotEmpty) {
String firstSectionId = _sectionProvider.sections.first.id;
// Use the first section ID for initial history fetch _learningHistory =
final firstSectionId = await sectionsResult await _repository.getLearningHistory(firstSectionId, token);
.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<LearningHistory>;
} else { } else {
_error = 'No sections available'; _error = 'No sections available';
} }
} catch (e) { } catch (e) {
_error = 'Error loading data: ${e.toString()}'; _error = 'Error loading data: ${e.toString()}';
_learningHistory = []; // Clear the list in case of error
} finally { } finally {
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
} }
} }
// Future<void> 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<LearningHistory>;
// } else {
// _error = 'No sections available';
// }
// } catch (e) {
// _error = 'Error loading data: ${e.toString()}';
// } finally {
// _isLoading = false;
// notifyListeners();
// }
// }
} }

View File

@ -10,48 +10,18 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class HistoryScreen extends StatefulWidget { class HistoryScreen extends StatelessWidget {
const HistoryScreen({ const HistoryScreen({super.key});
super.key,
});
@override
State<HistoryScreen> createState() => _HistoryScreenState();
}
class _HistoryScreenState extends State<HistoryScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final historyProvider =
Provider.of<HistoryProvider>(context, listen: false);
final userProvider = Provider.of<UserProvider>(context, listen: false);
historyProvider.fetchLearningHistory(userProvider.jwtToken!);
});
}
bool isNotFoundError(String error) { bool isNotFoundError(String error) {
return error.toLowerCase().contains('no learning history found') || return error.toLowerCase().contains('no learning history found') ||
error.toLowerCase().contains('not found'); error.toLowerCase().contains('not found');
} }
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance.addPostFrameCallback((_) {
// WidgetsBinding.instance.addPostFrameCallback((_) {
// final historyProvider =
// Provider.of<HistoryProvider>(context, listen: false);
// final userProvider = Provider.of<UserProvider>(context, listen: false);
// historyProvider.refreshData(userProvider.jwtToken!);
// });
// });
// }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<HistoryProvider>( return Consumer<HistoryProvider>(
builder: (context, historyProvider, chiild) { builder: (context, historyProvider, child) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.bgSoftColor, backgroundColor: AppColors.bgSoftColor,
body: SafeArea( body: SafeArea(
@ -68,7 +38,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
const CustomTabBar(), const CustomTabBar(),
const SizedBox(height: 8), const SizedBox(height: 8),
Expanded( Expanded(
child: _buildContent(historyProvider), child: _buildContent(context, historyProvider),
), ),
], ],
), ),
@ -76,10 +46,14 @@ class _HistoryScreenState extends State<HistoryScreen> {
), ),
), ),
); );
}); },
);
} }
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) { if (historyProvider.isLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
@ -87,7 +61,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
if (historyProvider.error != null) { if (historyProvider.error != null) {
return isNotFoundError(historyProvider.error!) return isNotFoundError(historyProvider.error!)
? _buildEmptyState(context) ? _buildEmptyState(context)
: _buildErrorState(historyProvider.error!); : _buildErrorState(context, historyProvider.error!);
} }
if (historyProvider.learningHistory.isEmpty) { if (historyProvider.learningHistory.isEmpty) {
@ -145,7 +119,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
); );
} }
Widget _buildErrorState(String error) { Widget _buildErrorState(BuildContext context, String error) {
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@ -206,6 +206,22 @@ class _HomeContentState extends State<HomeContent> {
); );
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer2<UserProvider, CompletedTopicsProvider>(builder: ( return Consumer2<UserProvider, CompletedTopicsProvider>(builder: (
@ -403,20 +419,27 @@ class _HomeContentState extends State<HomeContent> {
? _buildShimmerEffect() ? _buildShimmerEffect()
: completedTopicsProvider.completedTopics.isEmpty : completedTopicsProvider.completedTopics.isEmpty
? _buildNoDataWidget() ? _buildNoDataWidget()
: ListView.builder( : _buildCompletedTopicsContent(
shrinkWrap: true, completedTopicsProvider),
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric( // completedTopicsProvider.isLoading
horizontal: 16.0, // ? _buildShimmerEffect()
), // : completedTopicsProvider.completedTopics.isEmpty
itemCount: 1, // ? _buildNoDataWidget()
itemBuilder: (context, index) { // : ListView.builder(
return ProgressCard( // shrinkWrap: true,
completedTopic: completedTopicsProvider // physics: const NeverScrollableScrollPhysics(),
.completedTopics, // Kirim seluruh list // padding: const EdgeInsets.symmetric(
); // horizontal: 16.0,
}, // ),
), // itemCount: 1,
// itemBuilder: (context, index) {
// return ProgressCard(
// completedTopic: completedTopicsProvider
// .completedTopics, // Kirim seluruh list
// );
// },
// ),
], ],
), ),
), ),

View File

@ -1,5 +1,4 @@
import 'package:bootstrap_icons/bootstrap_icons.dart'; import 'package:english_learning/core/services/constants.dart';
import 'package:english_learning/core/services/repositories/constants.dart';
import 'package:english_learning/core/utils/styles/theme.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/models/completed_topics_model.dart';
import 'package:english_learning/features/home/widgets/progress_bar.dart'; import 'package:english_learning/features/home/widgets/progress_bar.dart';

View File

@ -1,3 +1,5 @@
import 'package:english_learning/features/learning/modules/exercises/models/review_exercise_model.dart';
class ExerciseModel { class ExerciseModel {
final String idAdminExercise; final String idAdminExercise;
final String idLevel; final String idLevel;
@ -61,21 +63,6 @@ class ExerciseModel {
choices: choices, choices: choices,
); );
} }
factory ExerciseModel.fromReviewJson(Map<String, dynamic> 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 { class MultipleChoice {
@ -138,6 +125,13 @@ class Pair {
final String right; final String right;
Pair({required this.left, required this.right}); Pair({required this.left, required this.right});
factory Pair.fromJson(Map<String, dynamic> json) {
return Pair(
left: json['LEFT_PAIR'] ?? '',
right: json['RIGHT_PAIR'] ?? '',
);
}
} }
class MatchingPair { class MatchingPair {
@ -147,17 +141,24 @@ class MatchingPair {
factory MatchingPair.fromJsonList(List? jsonList) { factory MatchingPair.fromJsonList(List? jsonList) {
if (jsonList == null) { if (jsonList == null) {
return MatchingPair( return MatchingPair(pairs: []);
pairs: []); // Return an empty list if jsonList is null
} }
List<Pair> pairs = []; List<Pair> pairs = jsonList.map((item) => Pair.fromJson(item)).toList();
for (var pair in jsonList) { return MatchingPair(pairs: pairs);
pairs.add(Pair(
left: pair['LEFT_PAIR'] ?? '', // Use empty string as default if null
right: pair['RIGHT_PAIR'] ?? '',
));
} }
// Helper methods
List<String> get leftPairs => pairs.map((pair) => pair.left).toList();
List<String> get rightPairs => pairs.map((pair) => pair.right).toList();
}
extension ReviewMatchingPairListExtension on List<ReviewMatchingPair> {
MatchingPair toMatchingPair() {
List<Pair> pairs = map((review) => Pair(
left: review.leftPair,
right: review.rightPair,
)).toList();
return MatchingPair(pairs: pairs); return MatchingPair(pairs: pairs);
} }
} }

View File

@ -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<ReviewExerciseDetail> 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<String, dynamic> 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<ReviewMultipleChoice>? multipleChoices;
final List<ReviewMatchingPair>? 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<String, dynamic> 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<ReviewMatchingPair> get matchingPairsOrEmpty {
return matchingPairs ?? [];
}
List<ReviewMultipleChoice> 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<String, dynamic> 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<String, dynamic> json) {
return ReviewMatchingPair(
leftPair: json['LEFT_PAIR'],
rightPair: json['RIGHT_PAIR'],
);
}
}

View File

@ -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:english_learning/features/learning/modules/feedback/models/feedback_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:english_learning/core/services/repositories/exercise_repository.dart'; import 'package:english_learning/core/services/repositories/exercise_repository.dart';
@ -14,6 +15,8 @@ class ExerciseProvider extends ChangeNotifier {
// State variables // State variables
List<ExerciseModel> _exercises = []; List<ExerciseModel> _exercises = [];
List<ReviewExerciseDetail> _reviewExercises = [];
ReviewExerciseModel? _reviewData;
Map<int, List<Map<String, String>>> _matchingAnswers = {}; Map<int, List<Map<String, String>>> _matchingAnswers = {};
List<String> _answers = []; List<String> _answers = [];
List<List<Color>> _leftColors = []; List<List<Color>> _leftColors = [];
@ -24,7 +27,6 @@ class ExerciseProvider extends ChangeNotifier {
String _nameLevel = ''; String _nameLevel = '';
String? _activeLeftOption; String? _activeLeftOption;
String? _studentLearningId; String? _studentLearningId;
bool _isReview = false;
// Constants // Constants
final List<Color> _pairColors = [ final List<Color> _pairColors = [
@ -44,7 +46,7 @@ class ExerciseProvider extends ChangeNotifier {
String get nameLevel => _nameLevel; String get nameLevel => _nameLevel;
String? get activeLeftOption => _activeLeftOption; String? get activeLeftOption => _activeLeftOption;
String? get studentLearningId => _studentLearningId; String? get studentLearningId => _studentLearningId;
bool get isReview => _isReview; List<ReviewExerciseDetail> get reviewExercises => _reviewExercises;
// Initialization methods // Initialization methods
void initializeAnswers() { void initializeAnswers() {
@ -79,7 +81,6 @@ class ExerciseProvider extends ChangeNotifier {
// } // }
// } // }
void answerQuestion(int index, String answer) { void answerQuestion(int index, String answer) {
if (_isReview) return;
if (index >= 0 && index < _answers.length) { if (index >= 0 && index < _answers.length) {
if (_exercises[index].choices is MatchingPair) { if (_exercises[index].choices is MatchingPair) {
_handleMatchingPairAnswer(index, answer); _handleMatchingPairAnswer(index, answer);
@ -208,20 +209,30 @@ class ExerciseProvider extends ChangeNotifier {
void goToExercise(int index) { void goToExercise(int index) {
if (index >= 0 && index < _exercises.length) { if (index >= 0 && index < _exercises.length) {
_currentExerciseIndex = index; _currentExerciseIndex = index;
print('Going to exercise: $_currentExerciseIndex'); // Debug print
notifyListeners(); notifyListeners();
} }
} }
void nextQuestion() { 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++; _currentExerciseIndex++;
print('Moving to next question: $_currentExerciseIndex'); // Debug print
notifyListeners(); notifyListeners();
} }
} }
void previousQuestion() { void previousQuestion() {
print(
'Current index before previous: $_currentExerciseIndex'); // Debug print
if (_currentExerciseIndex > 0) { if (_currentExerciseIndex > 0) {
_currentExerciseIndex--; _currentExerciseIndex--;
print(
'Moving to previous question: $_currentExerciseIndex'); // Debug print
notifyListeners(); notifyListeners();
} }
} }
@ -245,6 +256,7 @@ class ExerciseProvider extends ChangeNotifier {
// API methods // API methods
Future<void> fetchExercises(String levelId) async { Future<void> fetchExercises(String levelId) async {
_isLoading = true; _isLoading = true;
_currentExerciseIndex = 0;
notifyListeners(); notifyListeners();
try { try {
@ -270,33 +282,93 @@ class ExerciseProvider extends ChangeNotifier {
} }
} }
// Di dalam ExerciseProvider class
Future<void> fetchReviewExercises(String stdLearningId) async { Future<void> fetchReviewExercises(String stdLearningId) async {
_isLoading = true; _isLoading = true;
_isReview = true; _currentExerciseIndex = 0;
notifyListeners(); notifyListeners();
try { try {
final token = await _userProvider.getValidToken(); final token = await _userProvider.getValidToken();
if (token == null) { if (token == null) throw Exception('No valid token found');
throw Exception('No valid token found');
}
final data = await _repository.getStudentAnswers(stdLearningId, token); final response =
_nameTopic = data['NAME_TOPIC']; await _repository.getStudentAnswers(stdLearningId, token);
_nameLevel = data['NAME_LEVEL'];
final exercisesData = data['stdExercises']; // Parse the response data
_exercises = exercisesData final payload = response['payload'];
.map<ExerciseModel>((json) => ExerciseModel.fromReviewJson(json)) _reviewData = ReviewExerciseModel.fromJson(payload);
.toList();
_answers = _exercises.map((e) => e.answerStudent ?? '').toList(); // 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) { } catch (e) {
print('Error fetching review exercises: $e'); print('Error fetching review exercises: $e');
rethrow;
} finally { } finally {
_isLoading = false; _isLoading = false;
notifyListeners(); 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<Map<String, dynamic>> submitAnswersAndGetScore() async { Future<Map<String, dynamic>> submitAnswersAndGetScore() async {
print('submitAnswersAndGetScore called'); print('submitAnswersAndGetScore called');
try { try {
@ -398,6 +470,7 @@ class ExerciseProvider extends ChangeNotifier {
_nameTopic = previous._nameTopic; _nameTopic = previous._nameTopic;
_nameLevel = previous._nameLevel; _nameLevel = previous._nameLevel;
_studentLearningId = previous._studentLearningId; _studentLearningId = previous._studentLearningId;
_reviewData = previous._reviewData;
} }
} }
} }

View File

@ -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/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_navigator.dart';
import 'package:english_learning/features/learning/modules/exercises/widgets/exercise_progress.dart'; import 'package:english_learning/features/learning/modules/exercises/widgets/exercise_progress.dart';
import 'package:english_learning/features/learning/modules/exercises/widgets/instruction_dialog.dart'; import 'package:english_learning/features/learning/modules/exercises/widgets/instruction_dialog.dart';
@ -31,6 +32,7 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
_scrollController = ScrollController(); _scrollController = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = context.read<ExerciseProvider>(); final provider = context.read<ExerciseProvider>();
if (widget.isReview) { if (widget.isReview) {
provider.fetchReviewExercises(widget.studentLearningId); provider.fetchReviewExercises(widget.studentLearningId);
} else { } else {
@ -62,11 +64,32 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
body: Center(child: CircularProgressIndicator()), 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( return Scaffold(
backgroundColor: AppColors.bgSoftColor, backgroundColor: AppColors.bgSoftColor,
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
automaticallyImplyLeading: false, 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), iconTheme: const IconThemeData(color: AppColors.whiteColor),
centerTitle: true, centerTitle: true,
title: Text( title: Text(
@ -81,41 +104,50 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
gradient: AppColors.gradientTheme, gradient: AppColors.gradientTheme,
), ),
), ),
actions: [ actions: widget.isReview
? [
IconButton( IconButton(
icon: const Icon(Icons.info_outline, color: AppColors.whiteColor), icon: const Icon(
BootstrapIcons.info_circle,
color: AppColors.whiteColor,
size: 22,
),
onPressed: () => _showInstructions(context), onPressed: () => _showInstructions(context),
), ),
], ]
: null,
), ),
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
if (!widget.isReview)
const Padding( const Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
child: ExerciseProgress(), child: ExerciseProgress(),
), ),
Expanded( Expanded(
child: RefreshIndicator(
onRefresh: () => provider.fetchExercises(widget.levelId!),
child: SingleChildScrollView( child: SingleChildScrollView(
controller: _scrollController, controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column( child: Column(
children: [ children: [
if (provider.isReview)
Text('Review Mode', style: TextStyle(fontSize: 24)),
const SizedBox(height: 16), const SizedBox(height: 16),
const ExerciseContent(), ExerciseContent(
key: ValueKey(provider.currentExerciseIndex),
exercise: currentExercise,
isReview: widget.isReview,
),
const SizedBox(height: 24), const SizedBox(height: 24),
ExerciseNavigator(onScrollToTop: _scrollToTop), ExerciseNavigator(
onScrollToTop: _scrollToTop,
isReview: widget.isReview,
),
const SizedBox(height: 32), const SizedBox(height: 32),
], ],
), ),
), ),
), ),
),
], ],
), ),
), ),

View File

@ -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<ExerciseContent> createState() => _ExerciseContentState();
}
class _ExerciseContentState extends State<ExerciseContent>
with WidgetsBindingObserver {
final GlobalKey<VideoPlayerWidgetState> _videoPlayerKey = GlobalKey();
final GlobalKey<AudioPlayerWidgetState> _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<ExerciseProvider>(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<double> animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
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,
),
),
],
);
}
}
}

View File

@ -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<ExerciseProvider>(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<double>(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,
)
],
),
),
),
);
});
}
}

View File

@ -1,4 +1,5 @@
import 'package:english_learning/core/utils/styles/theme.dart'; 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/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/complete_submission.dart';
import 'package:english_learning/features/learning/modules/exercises/widgets/incomplete_submission.dart'; import 'package:english_learning/features/learning/modules/exercises/widgets/incomplete_submission.dart';
@ -9,9 +10,12 @@ import 'package:provider/provider.dart';
class ExerciseNavigator extends StatelessWidget { class ExerciseNavigator extends StatelessWidget {
final VoidCallback? onScrollToTop; final VoidCallback? onScrollToTop;
final bool isReview;
const ExerciseNavigator({ const ExerciseNavigator({
super.key, super.key,
required this.onScrollToTop, required this.onScrollToTop,
this.isReview = false,
}); });
@override @override
@ -19,15 +23,18 @@ class ExerciseNavigator extends StatelessWidget {
return Consumer2<ExerciseProvider, UserProvider>( return Consumer2<ExerciseProvider, UserProvider>(
builder: (context, exerciseProvider, userProvider, _) { builder: (context, exerciseProvider, userProvider, _) {
final currentExerciseIndex = exerciseProvider.currentExerciseIndex; final currentExerciseIndex = exerciseProvider.currentExerciseIndex;
final totalExercises = isReview
? exerciseProvider.reviewExercises.length
: exerciseProvider.exercises.length;
final isFirstExercise = currentExerciseIndex == 0; final isFirstExercise = currentExerciseIndex == 0;
final isLastExercise = final isLastExercise = currentExerciseIndex == totalExercises - 1;
currentExerciseIndex == exerciseProvider.exercises.length - 1;
Future<void> submitAnswers() async { Future<void> submitAnswers() async {
try { try {
final result = await exerciseProvider.submitAnswersAndGetScore(); final result = await exerciseProvider.submitAnswersAndGetScore();
print('Submit result: $result'); // Logging print('Submit result: $result');
if (context.mounted) {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ResultScreen( builder: (context) => ResultScreen(
@ -39,91 +46,71 @@ class ExerciseNavigator extends StatelessWidget {
), ),
), ),
); );
}
} catch (e) { } catch (e) {
print('Error submitting answers: $e'); // Logging print('Error submitting answers: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')), SnackBar(content: Text('Error: $e')),
); );
} }
} }
}
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (!isFirstExercise) if (!isFirstExercise)
Expanded( Expanded(
child: ElevatedButton( flex: 3,
child: GlobalButton(
text: 'Previous',
height: 45,
onPressed: () { onPressed: () {
exerciseProvider.previousQuestion(); exerciseProvider.previousQuestion();
onScrollToTop?.call(); onScrollToTop?.call();
}, },
style: ElevatedButton.styleFrom(
shadowColor: Colors.transparent,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
shape: RoundedRectangleBorder( borderColor: AppColors.blueColor,
borderRadius: BorderRadius.circular(12), textColor: AppColors.blueColor,
side: const BorderSide( transparentBackground: true,
color: AppColors.blueColor,
),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Text(
'Previous',
style: AppTextStyles.blueTextStyle.copyWith(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
), ),
), ),
if (!isFirstExercise && !isLastExercise) const SizedBox(width: 8), if (!isFirstExercise && !isLastExercise) const SizedBox(width: 8),
if (!isLastExercise) if (!isLastExercise)
Expanded( Expanded(
child: ElevatedButton( flex: 3,
child: GlobalButton(
text: 'Next',
height: 45,
onPressed: () { onPressed: () {
exerciseProvider.nextQuestion(); exerciseProvider.nextQuestion();
onScrollToTop?.call(); onScrollToTop?.call();
}, },
style: ElevatedButton.styleFrom(
shadowColor: Colors.transparent,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
shape: RoundedRectangleBorder( borderColor: AppColors.blueColor,
borderRadius: BorderRadius.circular(12), textColor: AppColors.blueColor,
side: const BorderSide( transparentBackground: true,
color: AppColors.blueColor,
),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Text(
'Next',
style: AppTextStyles.blueTextStyle.copyWith(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
), ),
), ),
if (isLastExercise && !isFirstExercise) const SizedBox(width: 8), if (isLastExercise && !isFirstExercise) const SizedBox(width: 8),
if (isLastExercise) if (isLastExercise)
Expanded( Expanded(
child: ElevatedButton( flex: 5,
child: GlobalButton(
text: isReview ? 'Back to Level List' : 'Submit',
height: 45,
onPressed: () { onPressed: () {
if (isReview) {
Navigator.of(context).pop();
} else {
if (exerciseProvider.hasAnsweredQuestions()) { if (exerciseProvider.hasAnsweredQuestions()) {
if (exerciseProvider.hasUnansweredQuestions()) { if (exerciseProvider.hasUnansweredQuestions()) {
// Ada pertanyaan yang belum dijawab
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return IncompleteSubmission( return IncompleteSubmission(
onCheckAgain: () { onCheckAgain: () => Navigator.of(context).pop(),
Navigator.of(context).pop();
},
onSubmit: () async { onSubmit: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
submitAnswers(); submitAnswers();
@ -132,14 +119,11 @@ class ExerciseNavigator extends StatelessWidget {
}, },
); );
} else { } else {
// Semua pertanyaan sudah dijawab
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return CompleteSubmission( return CompleteSubmission(
onCheckAgain: () { onCheckAgain: () => Navigator.of(context).pop(),
Navigator.of(context).pop();
},
onSubmit: () async { onSubmit: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
submitAnswers(); submitAnswers();
@ -149,44 +133,22 @@ class ExerciseNavigator extends StatelessWidget {
); );
} }
} else { } else {
// Tidak ada pertanyaan yang dijawab
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( const SnackBar(
content: Text( content: Text(
'Please answer at least one question before submitting.')), '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, 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,
),
),
),
),
),
), ),
), ),
], ],
); );
}); },
);
} }
} }

View File

@ -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:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
@ -11,7 +13,6 @@ class InstructionsDialog extends StatelessWidget {
elevation: 0, elevation: 0,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
child: Container( child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.whiteColor, color: AppColors.whiteColor,
shape: BoxShape.rectangle, shape: BoxShape.rectangle,
@ -24,6 +25,8 @@ class InstructionsDialog extends StatelessWidget {
), ),
], ],
), ),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -35,50 +38,57 @@ class InstructionsDialog extends StatelessWidget {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Divider(
thickness: 0.5,
),
Padding(
padding: const EdgeInsets.only(top: 32, left: 32, right: 32),
child: Column(
children: [
_buildInstructionItem( _buildInstructionItem(
icon: Icons.question_answer, icon: BootstrapIcons.chat_square_text,
text: 'Answer all questions to complete the exercise.', text: 'Answer all questions to complete the exercise.',
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildInstructionItem( _buildInstructionItem(
icon: Icons.compare_arrows, icon: BootstrapIcons.arrow_left_right,
text: text:
'For matching pairs, select a left option first, then match it with a right option.', 'For matching pairs, select a left option first, then match it with a right option.',
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildInstructionItem( _buildInstructionItem(
icon: Icons.edit, icon: BootstrapIcons.pencil,
text: text:
'You can change your answers at any time before submitting.', 'You can change your answers at any time before submitting.',
), ),
const SizedBox(height: 24), const SizedBox(height: 32),
ElevatedButton( GlobalButton(
child: const Text('Got it!'), text: 'Got It',
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.blueColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
), ),
padding: ],
const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
), ),
), ),
], ],
), ),
), ),
),
); );
} }
Widget _buildInstructionItem({required IconData icon, required String text}) { Widget _buildInstructionItem({required IconData icon, required String text}) {
return Row( return Row(
children: [ children: [
Icon(icon, color: AppColors.blueColor), Icon(
const SizedBox(width: 12), icon,
color: AppColors.blueColor,
size: 24,
),
const SizedBox(width: 16),
Expanded( Expanded(
child: Text( child: Text(
text, text,
style: AppTextStyles.blackTextStyle.copyWith(fontSize: 16), style: AppTextStyles.greyTextStyle.copyWith(fontSize: 14),
), ),
), ),
], ],

View File

@ -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:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.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'; import 'package:provider/provider.dart';
class MatchingPairsQuestion extends StatelessWidget { class MatchingPairsQuestion extends StatelessWidget {
final ExerciseModel exercise; final dynamic exercise;
final bool isReview;
const MatchingPairsQuestion({ const MatchingPairsQuestion({
super.key, super.key,
required this.exercise, required this.exercise,
this.isReview = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final provider = Provider.of<ExerciseProvider>(context); final provider = Provider.of<ExerciseProvider>(context);
final matchingPair = exercise.choices as MatchingPair; List<dynamic> pairs = [];
Map<String, String> studentAnswers = {};
final currentIndex = provider.currentExerciseIndex; 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<String, String> reverseStudentAnswers = {};
studentAnswers.forEach((left, right) {
reverseStudentAnswers[right] = left;
});
return Column( return Column(
children: [ children: [
...matchingPair.pairs.asMap().entries.map((entry) { ...pairs.asMap().entries.map((entry) {
int pairIndex = entry.key; int pairIndex = entry.key;
Pair pair = entry.value; dynamic pair = entry.value;
return Row( 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, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( Expanded(
child: _buildOptionItem( child: _buildOptionItem(
context, context,
pair.left, left,
provider, provider,
currentIndex, currentIndex,
pairIndex, pairIndex,
true, true,
studentMatchedRight != null,
studentAnswers,
pairColor,
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: _buildOptionItem( child: _buildOptionItem(
context, context,
pair.right, right,
provider, provider,
currentIndex, currentIndex,
pairIndex, matchedPairIndex ?? pairIndex,
false, false,
matchedLeftForRight != null,
studentAnswers,
matchedPairIndex != null
? _getPairColor(matchedPairIndex)
: pairColor,
), ),
), ),
], ],
),
); );
}).toList(), }).toList(),
], ],
); );
} }
Color _getPairColor(int index) {
final List<Color> colors = [
Colors.blue,
Colors.green,
Colors.orange,
Colors.purple,
Colors.red,
];
return colors[index % colors.length];
}
Widget _buildOptionItem( Widget _buildOptionItem(
BuildContext context, BuildContext context,
String option, String option,
@ -61,29 +138,49 @@ class MatchingPairsQuestion extends StatelessWidget {
int exerciseIndex, int exerciseIndex,
int pairIndex, int pairIndex,
bool isLeft, bool isLeft,
bool isMatched,
Map<String, String> studentAnswers,
Color pairColor,
) { ) {
Color? color = isLeft Color? color;
? provider.getLeftColor(exerciseIndex, pairIndex) bool isSelected = false;
: provider.getRightColor(exerciseIndex, pairIndex); bool isActive = false;
final isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft);
final isActive = isLeft && provider.activeLeftOption == option; 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( return GestureDetector(
onTap: () { onTap: isReview
? null
: () {
if (!isLeft && provider.activeLeftOption == null) { if (!isLeft && provider.activeLeftOption == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please select a left option first.')), const SnackBar(
content: Text('Please select a left option first.'),
duration: Duration(seconds: 1),
),
); );
} else { } else {
provider.answerQuestion(exerciseIndex, option); provider.answerQuestion(exerciseIndex, option);
} }
}, },
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: isSelected
isSelected ? color : (isActive ? color! : AppColors.whiteColor), ? color ?? pairColor
: (isActive ? pairColor : AppColors.whiteColor),
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topRight: Radius.circular(20), topRight: Radius.circular(20),
bottomRight: Radius.circular(20), bottomRight: Radius.circular(20),
@ -95,11 +192,10 @@ class MatchingPairsQuestion extends StatelessWidget {
: AppColors.cardDisabledColor, : AppColors.cardDisabledColor,
width: isSelected ? 2 : 1, width: isSelected ? 2 : 1,
), ),
boxShadow: isActive boxShadow: isActive && !isReview
? [ ? [
BoxShadow( BoxShadow(
color: color?.withOpacity(0.5) ?? color: pairColor.withOpacity(0.5),
Colors.grey.withOpacity(0.5),
spreadRadius: 1, spreadRadius: 1,
blurRadius: 4, blurRadius: 4,
offset: const Offset(0, 2), offset: const Offset(0, 2),
@ -118,7 +214,6 @@ class MatchingPairsQuestion extends StatelessWidget {
), ),
), ),
), ),
),
); );
} }
} }

View File

@ -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:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.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'; import 'package:provider/provider.dart';
class MultipleChoiceQuestion extends StatelessWidget { class MultipleChoiceQuestion extends StatelessWidget {
final ExerciseModel exercise; final dynamic exercise;
final bool isReview;
const MultipleChoiceQuestion({ const MultipleChoiceQuestion({
super.key, super.key,
required this.exercise, required this.exercise,
this.isReview = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final provider = Provider.of<ExerciseProvider>(context); final provider = Provider.of<ExerciseProvider>(context);
List<String> options = [];
String? studentAnswer;
if (exercise is ExerciseModel) {
final multipleChoice = exercise.choices as MultipleChoice; final multipleChoice = exercise.choices as MultipleChoice;
final options = [ options = [
multipleChoice.optionA, multipleChoice.optionA,
multipleChoice.optionB, multipleChoice.optionB,
multipleChoice.optionC, multipleChoice.optionC,
multipleChoice.optionD, multipleChoice.optionD,
multipleChoice.optionE, multipleChoice.optionE,
]; ];
} else if (exercise is ReviewExerciseDetail) {
return _buildOptionsList(options, provider); if (exercise.multipleChoices?.isNotEmpty ?? false) {
final choices = exercise.multipleChoices!.first;
options = [
choices.optionA,
choices.optionB,
choices.optionC,
choices.optionD,
choices.optionE,
];
studentAnswer = exercise.answerStudent;
}
} }
Widget _buildOptionsList(List<String> options, ExerciseProvider provider) { return _buildOptionsList(context, options, provider, studentAnswer);
final optionLabels = List.generate( }
options.length,
(index) =>
String.fromCharCode(65 + index), // Generate labels "A", "B", etc.
);
return ListView.builder( Widget _buildOptionsList(
shrinkWrap: true, BuildContext context,
physics: const NeverScrollableScrollPhysics(), List<String> options,
itemCount: options.length, ExerciseProvider provider,
itemBuilder: (context, i) { String? studentAnswer,
final option = options[i]; ) {
final isSelected = return Column(
provider.answers[provider.currentExerciseIndex] == optionLabels[i]; 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
bool isSelected = false;
if (isReview) {
isSelected = studentAnswer == optionLabel;
} else {
isSelected =
provider.answers[provider.currentExerciseIndex] == optionLabel;
}
return GestureDetector( return GestureDetector(
onTap: () => onTap: isReview
provider.answerQuestion(provider.currentExerciseIndex, option), ? null
child: _buildOptionItem(optionLabels[i], option, isSelected), : () {
); provider.answerQuestion(
provider.currentExerciseIndex, option);
}, },
child: _buildOptionItem(optionLabel, option, isSelected),
);
}).toList(),
); );
} }
Widget _buildOptionItem(String label, String option, bool isSelected) { Widget _buildOptionItem(String label, String option, bool isSelected) {
final backgroundColor =
isSelected ? AppColors.blueColor : AppColors.whiteColor;
final textColor = isSelected ? AppColors.whiteColor : AppColors.blackColor;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0), padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row( child: Row(
@ -60,7 +92,7 @@ class MultipleChoiceQuestion extends StatelessWidget {
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? AppColors.blueColor : AppColors.whiteColor, color: backgroundColor,
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
border: Border.all( border: Border.all(
color: isSelected color: isSelected
@ -68,16 +100,13 @@ class MultipleChoiceQuestion extends StatelessWidget {
: AppColors.cardDisabledColor, : AppColors.cardDisabledColor,
), ),
), ),
child: Padding(
padding: padding:
const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0), const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
child: Text( child: Text(
label, label,
style: AppTextStyles.blackTextStyle.copyWith( style: AppTextStyles.blackTextStyle.copyWith(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: color: textColor,
isSelected ? AppColors.whiteColor : AppColors.blackColor,
),
), ),
), ),
), ),
@ -85,9 +114,8 @@ class MultipleChoiceQuestion extends StatelessWidget {
Expanded( Expanded(
child: Container( child: Container(
margin: const EdgeInsets.only(bottom: 8.0), margin: const EdgeInsets.only(bottom: 8.0),
width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? AppColors.blueColor : AppColors.whiteColor, color: backgroundColor,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topRight: Radius.circular(20), topRight: Radius.circular(20),
bottomRight: Radius.circular(20), bottomRight: Radius.circular(20),
@ -104,8 +132,7 @@ class MultipleChoiceQuestion extends StatelessWidget {
child: Text( child: Text(
option, option,
style: AppTextStyles.blackTextStyle.copyWith( style: AppTextStyles.blackTextStyle.copyWith(
color: color: textColor,
isSelected ? AppColors.whiteColor : AppColors.blackColor,
), ),
), ),
), ),

View File

@ -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: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: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 { class TrueFalseQuestion extends StatelessWidget {
final ExerciseModel exercise; final dynamic exercise;
final bool isReview;
const TrueFalseQuestion({ const TrueFalseQuestion({
super.key, super.key,
required this.exercise, required this.exercise,
this.isReview = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final provider = Provider.of<ExerciseProvider>(context); final provider = Provider.of<ExerciseProvider>(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<String> options, ExerciseProvider provider) { String? _getStudentAnswer() {
return ListView.builder( if (isReview && exercise is ReviewExerciseDetail) {
shrinkWrap: true, return exercise.answerStudent;
physics: const NeverScrollableScrollPhysics(), }
itemCount: options.length, return null;
itemBuilder: (context, i) { }
final option = options[i];
final isSelected = provider.answers[provider.currentExerciseIndex] == Widget _buildOptionsList(
(i == 0 ? '1' : '0'); BuildContext context,
List<Map<String, String>> 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( return GestureDetector(
// onTap: () => onTap: isReview
// provider.answerQuestion(provider.currentExerciseIndex, option), ? null
onTap: () => provider.answerQuestion( : () {
provider.currentExerciseIndex, i == 0 ? '1' : '0'), provider.answerQuestion(provider.currentExerciseIndex, value);
child: _buildOptionItem(option, isSelected),
);
}, },
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( return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0), padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row( child: Row(
@ -51,7 +92,7 @@ class TrueFalseQuestion extends StatelessWidget {
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? AppColors.blueColor : AppColors.whiteColor, color: backgroundColor,
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
border: Border.all( border: Border.all(
color: isSelected color: isSelected
@ -59,18 +100,13 @@ class TrueFalseQuestion extends StatelessWidget {
: AppColors.cardDisabledColor, : AppColors.cardDisabledColor,
), ),
), ),
child: Padding( padding:
padding: const EdgeInsets.symmetric( const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
horizontal: 14.0,
vertical: 10.0,
),
child: Text( child: Text(
option, label,
style: AppTextStyles.blackTextStyle.copyWith( style: AppTextStyles.blackTextStyle.copyWith(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: color: textColor,
isSelected ? AppColors.whiteColor : AppColors.blackColor,
),
), ),
), ),
), ),
@ -78,9 +114,8 @@ class TrueFalseQuestion extends StatelessWidget {
Expanded( Expanded(
child: Container( child: Container(
margin: const EdgeInsets.only(bottom: 8.0), margin: const EdgeInsets.only(bottom: 8.0),
width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? AppColors.blueColor : AppColors.whiteColor, color: backgroundColor,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topRight: Radius.circular(20), topRight: Radius.circular(20),
bottomRight: Radius.circular(20), bottomRight: Radius.circular(20),
@ -95,10 +130,9 @@ class TrueFalseQuestion extends StatelessWidget {
padding: padding:
const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
child: Text( child: Text(
option, text,
style: AppTextStyles.blackTextStyle.copyWith( style: AppTextStyles.blackTextStyle.copyWith(
color: color: textColor,
isSelected ? AppColors.whiteColor : AppColors.blackColor,
), ),
), ),
), ),

View File

@ -71,22 +71,6 @@ class LevelProvider with ChangeNotifier {
// _lastCompletedLevel!['ID_LEVEL'] == levelId; // _lastCompletedLevel!['ID_LEVEL'] == levelId;
// } // }
Future<void> 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) { bool isPretestFinished(String levelId) {
return _levels.any( return _levels.any(
(level) => level.idLevel == levelId && level.idStudentLearning != null); (level) => level.idLevel == levelId && level.idStudentLearning != null);

View File

@ -27,6 +27,32 @@ class PretestCard extends StatelessWidget {
final isFinished = levelProvider.isPretestFinished(pretest.idLevel); final isFinished = levelProvider.isPretestFinished(pretest.idLevel);
final score = levelProvider.getPretestScore(pretest.idLevel); final score = levelProvider.getPretestScore(pretest.idLevel);
// final isAllowed = levelProvider.isLevelAllowed(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( return Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@ -140,17 +166,7 @@ class PretestCard extends StatelessWidget {
// ); // );
// } // }
// : () {}, // : () {},
onPressed: () { onPressed: navigateToMaterial,
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MaterialScreen(
levelId: pretest.idLevel,
isReview: isFinished,
),
),
);
},
), ),
], ],
), ),

View File

@ -1,5 +1,5 @@
import 'package:english_learning/core/services/dio_client.dart'; 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/services/repositories/student_learning_repository.dart';
import 'package:english_learning/core/widgets/global_button.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/auth/provider/user_provider.dart';
@ -15,11 +15,13 @@ import 'package:provider/provider.dart';
class MaterialScreen extends StatefulWidget { class MaterialScreen extends StatefulWidget {
final String levelId; final String levelId;
final bool isReview; final bool isReview;
final String? studentLearningId;
const MaterialScreen({ const MaterialScreen({
super.key, super.key,
required this.levelId, required this.levelId,
this.isReview = false, this.isReview = false,
this.studentLearningId,
}); });
@override @override
@ -59,6 +61,11 @@ class _MaterialScreenState extends State<MaterialScreen>
} }
Future<void> _createStudentLearning() async { Future<void> _createStudentLearning() async {
if (widget.isReview && widget.studentLearningId != null) {
// Jika mode review dan studentLearningId tersedia, langsung navigasi ke ExerciseScreen
_navigateToExercise(widget.studentLearningId!);
return;
}
setState(() { setState(() {
_isLoading = true; _isLoading = true;
}); });
@ -77,15 +84,7 @@ class _MaterialScreenState extends State<MaterialScreen>
print('Student Learning created: ${result['message']}'); print('Student Learning created: ${result['message']}');
// Navigate to ExerciseScreen // Navigate to ExerciseScreen
Navigator.pushReplacement( _navigateToExercise(result['payload']['ID_STUDENT_LEARNING']);
context,
MaterialPageRoute(
builder: (context) => ExerciseScreen(
levelId: widget.levelId,
studentLearningId: result['payload']['ID_STUDENT_LEARNING'],
),
),
);
} catch (e) { } catch (e) {
// Show error message // Show error message
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -98,6 +97,19 @@ class _MaterialScreenState extends State<MaterialScreen>
} }
} }
void _navigateToExercise(String studentLearningId) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => ExerciseScreen(
levelId: widget.levelId,
studentLearningId: studentLearningId,
isReview: widget.isReview,
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<LevelProvider>(builder: (context, levelProvider, child) { return Consumer<LevelProvider>(builder: (context, levelProvider, child) {

View File

@ -16,21 +16,48 @@ class VideoPlayerWidget extends StatefulWidget {
} }
class VideoPlayerWidgetState extends State<VideoPlayerWidget> { class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
late Widget _videoWidget;
VideoPlayerController? _videoController; VideoPlayerController? _videoController;
YoutubePlayerController? _youtubeController; YoutubePlayerController? _youtubeController;
FlickManager? _flickManager; FlickManager? _flickManager;
bool _isLoading = true; bool _isLoading = true;
String? _error; String? _error;
bool _isYoutubeReady = false;
bool get wantKeepAlive => true; String? _youtubeId;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_youtubeId = _extractYoutubeId(widget.videoUrl);
_initializeVideoPlayerWidget(); _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) { String _getPlayableUrl(String url) {
if (url.contains('drive.google.com')) { if (url.contains('drive.google.com')) {
final regex = RegExp(r'/d/([a-zA-Z0-9-_]+)'); final regex = RegExp(r'/d/([a-zA-Z0-9-_]+)');
@ -43,54 +70,66 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
return url; return url;
} }
void _initializeVideoPlayerWidget() { Future<void> _initializeVideoPlayerWidget() async {
if (YoutubePlayer.convertUrlToId(widget.videoUrl) != null) { if (_youtubeId != null) {
try {
_youtubeController = YoutubePlayerController( _youtubeController = YoutubePlayerController(
initialVideoId: YoutubePlayer.convertUrlToId(widget.videoUrl)!, initialVideoId: _youtubeId!,
flags: const YoutubePlayerFlags( flags: const YoutubePlayerFlags(
autoPlay: false, autoPlay: false,
mute: false, mute: false,
hideControls: false,
controlsVisibleAtStart: true,
enableCaption: true,
useHybridComposition: true,
forceHD: true,
), ),
); );
_videoWidget = YoutubePlayer( if (mounted) {
controller: _youtubeController!, setState(() {}); // Trigger rebuild with controller
showVideoProgressIndicator: true, }
onReady: () { } catch (e) {
_youtubeController!.addListener(_youtubeListener); if (mounted) {
setState(() { setState(() {
_error = "Error initializing YouTube player: $e";
_isLoading = false; _isLoading = false;
}); });
}, }
); }
} else { } else {
try {
_videoController = VideoPlayerController.networkUrl( _videoController = VideoPlayerController.networkUrl(
Uri.parse(_getPlayableUrl(widget.videoUrl)), Uri.parse(_getPlayableUrl(widget.videoUrl)),
); );
_videoController!.initialize().then((_) {
await _videoController!.initialize();
_flickManager = FlickManager( _flickManager = FlickManager(
videoPlayerController: _videoController!, videoPlayerController: _videoController!,
autoPlay: false, autoPlay: false,
); );
_videoWidget = FlickVideoPlayer(
flickManager: _flickManager!, if (mounted) {
);
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
}).catchError((error) { }
} catch (e) {
if (mounted) {
setState(() { setState(() {
_error = "Error loading video: $error"; _error = "Error initializing video player: $e";
_isLoading = false; _isLoading = false;
}); });
}); }
}
} }
} }
void _youtubeListener() { void _youtubeListener() {
if (_youtubeController!.value.playerState == PlayerState.ended) { if (_youtubeController?.value.playerState == PlayerState.ended) {
_youtubeController!.seekTo(Duration.zero); _youtubeController?.seekTo(Duration.zero);
_youtubeController!.pause(); _youtubeController?.pause();
} }
} }
@ -104,18 +143,103 @@ class VideoPlayerWidgetState extends State<VideoPlayerWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading) { if (_youtubeId != null && _youtubeController != null) {
return const Center(child: CircularProgressIndicator());
} else if (_error != null) {
return Center(child: Text(_error!));
}
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: _videoWidget, 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<Color>(Colors.white),
),
),
), ),
); );
},
),
),
);
}
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: AspectRatio(
aspectRatio: 16 / 9,
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<Color>(Colors.white),
),
);
}
if (_flickManager != null) {
return FlickVideoPlayer(flickManager: _flickManager!);
}
return const SizedBox.shrink();
} }
void stopAndResetVideo() { void stopAndResetVideo() {

View File

@ -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/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/level/screens/level_list_screen.dart';
import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart'; import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart';

View File

@ -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/provider/section_provider.dart';
import 'package:english_learning/features/learning/widgets/section_card.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/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:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class LearningScreen extends StatefulWidget { class LearningScreen extends StatefulWidget {
const LearningScreen({ const LearningScreen({
@ -102,9 +102,7 @@ class _LearningScreenState extends State<LearningScreen> {
return ListView.builder( return ListView.builder(
itemCount: 5, // Misalnya, kita menampilkan 5 shimmer items itemCount: 5, // Misalnya, kita menampilkan 5 shimmer items
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Shimmer.fromColors( return ShimmerWidget(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container( child: Container(
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
child: Row( child: Row(

View File

@ -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:english_learning/features/learning/modules/model/section_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/utils/styles/theme.dart';
class LearningCard extends StatelessWidget { class LearningCard extends StatefulWidget {
final Section section; final Section section;
final VoidCallback? onTap; final VoidCallback? onTap;
@ -13,6 +13,12 @@ class LearningCard extends StatelessWidget {
this.onTap, this.onTap,
}); });
@override
State<LearningCard> createState() => _LearningCardState();
}
class _LearningCardState extends State<LearningCard>
with SingleTickerProviderStateMixin {
String _getFullImageUrl(String thumbnail) { String _getFullImageUrl(String thumbnail) {
if (thumbnail.startsWith('http')) { if (thumbnail.startsWith('http')) {
return thumbnail; return thumbnail;
@ -24,7 +30,7 @@ class LearningCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: widget.onTap,
child: Card( child: Card(
color: AppColors.whiteColor, color: AppColors.whiteColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@ -40,7 +46,7 @@ class LearningCard extends StatelessWidget {
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Image.network( child: Image.network(
_getFullImageUrl(section.thumbnail), _getFullImageUrl(widget.section.thumbnail),
width: 90, width: 90,
height: 104, height: 104,
fit: BoxFit.cover, fit: BoxFit.cover,
@ -72,7 +78,7 @@ class LearningCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
section.name, widget.section.name,
style: AppTextStyles.blackTextStyle.copyWith( style: AppTextStyles.blackTextStyle.copyWith(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
@ -80,7 +86,7 @@ class LearningCard extends StatelessWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
section.description, widget.section.description,
style: AppTextStyles.disableTextStyle.copyWith( style: AppTextStyles.disableTextStyle.copyWith(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,

View File

@ -3,6 +3,7 @@ import 'package:shimmer/shimmer.dart';
class ShimmerWidget extends StatelessWidget { class ShimmerWidget extends StatelessWidget {
final Widget child; final Widget child;
const ShimmerWidget({ const ShimmerWidget({
super.key, super.key,
required this.child, required this.child,

View File

@ -1,5 +1,5 @@
import 'dart:io'; 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/utils/styles/theme.dart';
import 'package:english_learning/core/widgets/form_field/custom_field_widget.dart'; import 'package:english_learning/core/widgets/form_field/custom_field_widget.dart';
import 'package:english_learning/core/widgets/global_button.dart'; import 'package:english_learning/core/widgets/global_button.dart';
@ -60,7 +60,9 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
}; };
try { try {
userProvider.setLoading(true);
await userProvider.updateUserProfile(updatedData); await userProvider.updateUserProfile(updatedData);
userProvider.setLoading(false);
showDialog( showDialog(
context: context, context: context,
@ -186,7 +188,10 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
GlobalButton( GlobalButton(
text: 'Save Changes', text: 'Save Changes',
onPressed: () => _updateUserProfile(context), isLoading: userProvider.isLoading,
onPressed: userProvider.isLoading
? null
: () => _updateUserProfile(context),
), ),
], ],
), ),

View File

@ -1,5 +1,5 @@
import 'package:bootstrap_icons/bootstrap_icons.dart'; 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/provider/user_provider.dart';
import 'package:english_learning/features/auth/screens/signin/signin_screen.dart'; import 'package:english_learning/features/auth/screens/signin/signin_screen.dart';
import 'package:english_learning/features/settings/modules/change_password/screens/change_password_screen.dart'; import 'package:english_learning/features/settings/modules/change_password/screens/change_password_screen.dart';

View File

@ -676,10 +676,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider name: path_provider
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.5"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description: