Merge branch 'various-fix' into 'master'
Updated various files, including constants, exercise providers, question... See merge request profile-image/kedaireka/polinema-adapative-learning/mobile-adaptive-learning!15
This commit is contained in:
commit
e500ed0fa0
|
|
@ -1 +1 @@
|
||||||
const String baseUrl = 'https://8b78-114-6-25-184.ngrok-free.app/';
|
const String baseUrl = 'https://7333-114-6-25-184.ngrok-free.app/';
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ class DioClient {
|
||||||
DioClient() {
|
DioClient() {
|
||||||
_dio.options.baseUrl = baseUrl;
|
_dio.options.baseUrl = baseUrl;
|
||||||
_dio.options.connectTimeout = const Duration(seconds: 5);
|
_dio.options.connectTimeout = const Duration(seconds: 5);
|
||||||
_dio.options.receiveTimeout = const Duration(seconds: 3);
|
_dio.options.receiveTimeout = const Duration(seconds: 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> refreshAccessToken(String refreshToken) async {
|
Future<Response> refreshAccessToken(String refreshToken) async {
|
||||||
|
|
|
||||||
|
|
@ -439,8 +439,7 @@ class _HomeContentState extends State<HomeContent> {
|
||||||
),
|
),
|
||||||
completedTopicsProvider.isLoading
|
completedTopicsProvider.isLoading
|
||||||
? _buildShimmerEffect()
|
? _buildShimmerEffect()
|
||||||
: completedTopicsProvider.completedTopics == null ||
|
: completedTopicsProvider.completedTopics.isEmpty
|
||||||
completedTopicsProvider.completedTopics.isEmpty
|
|
||||||
? _buildNoDataWidget()
|
? _buildNoDataWidget()
|
||||||
: _buildCompletedTopicsContent(
|
: _buildCompletedTopicsContent(
|
||||||
completedTopicsProvider),
|
completedTopicsProvider),
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class ProgressCard extends StatelessWidget {
|
||||||
String _getFullImageUrl(String thumbnail) {
|
String _getFullImageUrl(String thumbnail) {
|
||||||
return thumbnail.startsWith('http')
|
return thumbnail.startsWith('http')
|
||||||
? thumbnail
|
? thumbnail
|
||||||
: '${baseUrl}uploads/section/$thumbnail';
|
: '${baseUrl}api/uploads/section/$thumbnail';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _navigateToTopics(
|
Future<void> _navigateToTopics(
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
String _nameLevel = '';
|
String _nameLevel = '';
|
||||||
String? _activeLeftOption;
|
String? _activeLeftOption;
|
||||||
String? _studentLearningId;
|
String? _studentLearningId;
|
||||||
|
List<List<dynamic>> _shuffledRightPairsList = [];
|
||||||
|
List<List<dynamic>> _savedShuffledRightPairsList = [];
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
final List<Color> _pairColors = [
|
final List<Color> _pairColors = [
|
||||||
|
|
@ -47,6 +49,9 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
String? get activeLeftOption => _activeLeftOption;
|
String? get activeLeftOption => _activeLeftOption;
|
||||||
String? get studentLearningId => _studentLearningId;
|
String? get studentLearningId => _studentLearningId;
|
||||||
List<ReviewExerciseDetail> get reviewExercises => _reviewExercises;
|
List<ReviewExerciseDetail> get reviewExercises => _reviewExercises;
|
||||||
|
List<List<dynamic>> get shuffledRightPairsList => _shuffledRightPairsList;
|
||||||
|
List<List<dynamic>> get savedShuffledRightPairsList =>
|
||||||
|
_savedShuffledRightPairsList;
|
||||||
|
|
||||||
// Initialization methods
|
// Initialization methods
|
||||||
void initializeAnswers() {
|
void initializeAnswers() {
|
||||||
|
|
@ -69,6 +74,35 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method untuk menyimpan urutan acak saat submit
|
||||||
|
void saveShuffledRightPairs() {
|
||||||
|
_savedShuffledRightPairsList = List.from(_shuffledRightPairsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method untuk mengatur ulang urutan acak saat review
|
||||||
|
void restoreShuffledRightPairs() {
|
||||||
|
if (_savedShuffledRightPairsList.isNotEmpty) {
|
||||||
|
_shuffledRightPairsList = List.from(_savedShuffledRightPairsList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initializeShuffledRightPairs(List<ExerciseModel> exercises) {
|
||||||
|
// Hanya acak sekali saat pertama kali membuka level
|
||||||
|
if (_shuffledRightPairsList.isEmpty) {
|
||||||
|
_shuffledRightPairsList = exercises.map((exercise) {
|
||||||
|
if (exercise.choices is MatchingPair) {
|
||||||
|
final pairs = (exercise.choices as MatchingPair).pairs;
|
||||||
|
final rightPairs = pairs.map((pair) => pair.right).toList();
|
||||||
|
|
||||||
|
// Buat salinan dan acak
|
||||||
|
final shuffledPairs = List.from(rightPairs)..shuffle();
|
||||||
|
return shuffledPairs;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// // Answer handling methods
|
// // Answer handling methods
|
||||||
// void answerQuestion(int index, String answer) {
|
// void answerQuestion(int index, String answer) {
|
||||||
// if (index >= 0 && index < _answers.length) {
|
// if (index >= 0 && index < _answers.length) {
|
||||||
|
|
@ -109,6 +143,20 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasMatchedPair(int exerciseIndex, String rightOption) {
|
||||||
|
if (!_matchingAnswers.containsKey(exerciseIndex)) return false;
|
||||||
|
return _matchingAnswers[exerciseIndex]!
|
||||||
|
.any((pair) => pair['right'] == rightOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getMatchedLeftPair(int exerciseIndex, String rightOption) {
|
||||||
|
if (!_matchingAnswers.containsKey(exerciseIndex)) return null;
|
||||||
|
final matchedPair = _matchingAnswers[exerciseIndex]!.firstWhere(
|
||||||
|
(pair) => pair['right'] == rightOption,
|
||||||
|
orElse: () => {'left': '', 'right': ''});
|
||||||
|
return matchedPair['left']!.isEmpty ? null : matchedPair['left'];
|
||||||
|
}
|
||||||
|
|
||||||
void _handleMatchingPairAnswer(int exerciseIndex, String option) {
|
void _handleMatchingPairAnswer(int exerciseIndex, String option) {
|
||||||
final matchingPair = _exercises[exerciseIndex].choices as MatchingPair;
|
final matchingPair = _exercises[exerciseIndex].choices as MatchingPair;
|
||||||
final isLeft = matchingPair.pairs.any((pair) => pair.left == option);
|
final isLeft = matchingPair.pairs.any((pair) => pair.left == option);
|
||||||
|
|
@ -137,27 +185,20 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final pairs = (_exercises[exerciseIndex].choices as MatchingPair).pairs;
|
if (!_matchingAnswers.containsKey(exerciseIndex)) {
|
||||||
final leftIndex =
|
_matchingAnswers[exerciseIndex] = [];
|
||||||
pairs.indexWhere((pair) => pair.left == _activeLeftOption);
|
|
||||||
final rightIndex = pairs.indexWhere((pair) => pair.right == option);
|
|
||||||
|
|
||||||
if (leftIndex != -1 && rightIndex != -1) {
|
|
||||||
if (!_matchingAnswers.containsKey(exerciseIndex)) {
|
|
||||||
_matchingAnswers[exerciseIndex] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
_matchingAnswers[exerciseIndex]!.removeWhere((pair) =>
|
|
||||||
pair['left'] == _activeLeftOption || pair['right'] == option);
|
|
||||||
|
|
||||||
_matchingAnswers[exerciseIndex]!
|
|
||||||
.add({'left': _activeLeftOption!, 'right': option});
|
|
||||||
|
|
||||||
_rightColors[exerciseIndex][rightIndex] =
|
|
||||||
_leftColors[exerciseIndex][leftIndex];
|
|
||||||
|
|
||||||
_activeLeftOption = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove any existing matches for this left or right option
|
||||||
|
_matchingAnswers[exerciseIndex]!.removeWhere(
|
||||||
|
(pair) => pair['left'] == _activeLeftOption || pair['right'] == option);
|
||||||
|
|
||||||
|
// Add the new pair
|
||||||
|
_matchingAnswers[exerciseIndex]!
|
||||||
|
.add({'left': _activeLeftOption!, 'right': option});
|
||||||
|
|
||||||
|
// Reset active left option
|
||||||
|
_activeLeftOption = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateMatchingPairAnswer(int exerciseIndex) {
|
void _updateMatchingPairAnswer(int exerciseIndex) {
|
||||||
|
|
@ -260,11 +301,16 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
|
|
||||||
// API methods
|
// API methods
|
||||||
Future<void> fetchExercises(String levelId) async {
|
Future<void> fetchExercises(String levelId) async {
|
||||||
|
if (_isLoading) return; // Hindari multiple calls
|
||||||
|
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_currentExerciseIndex = 0;
|
_currentExerciseIndex = 0;
|
||||||
_resetReviewState();
|
_resetReviewState();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
|
// Reset shuffled pairs
|
||||||
|
_shuffledRightPairsList = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final token = await _userProvider.getValidToken();
|
final token = await _userProvider.getValidToken();
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
|
|
@ -275,9 +321,16 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
_nameTopic = data['NAME_TOPIC'];
|
_nameTopic = data['NAME_TOPIC'];
|
||||||
_nameLevel = data['NAME_LEVEL'];
|
_nameLevel = data['NAME_LEVEL'];
|
||||||
final exercisesData = data['EXERCISES'];
|
final exercisesData = data['EXERCISES'];
|
||||||
|
|
||||||
|
// Map exercises
|
||||||
_exercises = exercisesData
|
_exercises = exercisesData
|
||||||
.map<ExerciseModel>((json) => ExerciseModel.fromJson(json))
|
.map<ExerciseModel>((json) => ExerciseModel.fromJson(json))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
// Inisialisasi pengacakan untuk matching pairs
|
||||||
|
initializeShuffledRightPairs(_exercises);
|
||||||
|
|
||||||
|
// Inisialisasi jawaban
|
||||||
_answers = List.generate(_exercises.length, (index) => '');
|
_answers = List.generate(_exercises.length, (index) => '');
|
||||||
initializeAnswers();
|
initializeAnswers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -305,6 +358,8 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
final payload = response['payload'];
|
final payload = response['payload'];
|
||||||
_reviewData = ReviewExerciseModel.fromJson(payload);
|
_reviewData = ReviewExerciseModel.fromJson(payload);
|
||||||
|
|
||||||
|
restoreShuffledRightPairs();
|
||||||
|
|
||||||
// Sort the exercises based on the title number
|
// Sort the exercises based on the title number
|
||||||
_reviewExercises = (_reviewData?.stdExercises ?? [])
|
_reviewExercises = (_reviewData?.stdExercises ?? [])
|
||||||
..sort((a, b) {
|
..sort((a, b) {
|
||||||
|
|
@ -376,7 +431,8 @@ class ExerciseProvider extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> submitAnswersAndGetScore() async {
|
Future<Map<String, dynamic>> submitAnswersAndGetScore() async {
|
||||||
print('submitAnswersAndGetScore called');
|
// Simpan urutan acak sebelum submit
|
||||||
|
saveShuffledRightPairs();
|
||||||
try {
|
try {
|
||||||
if (_studentLearningId == null) {
|
if (_studentLearningId == null) {
|
||||||
throw Exception('Student Learning ID is not set');
|
throw Exception('Student Learning ID is not set');
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ class _ExerciseContentState extends State<ExerciseContent>
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
child: ImageWidget(
|
child: ImageWidget(
|
||||||
imageFileName: widget.exercise.image!,
|
imageFileName: widget.exercise.image!,
|
||||||
baseUrl: '${baseUrl}uploads/exercise/image/',
|
baseUrl: '${baseUrl}api/uploads/exercise/image/',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -180,7 +180,7 @@ class _ExerciseContentState extends State<ExerciseContent>
|
||||||
child: AudioPlayerWidget(
|
child: AudioPlayerWidget(
|
||||||
key: _audioPlayerKey,
|
key: _audioPlayerKey,
|
||||||
audioFileName: widget.exercise.audio!,
|
audioFileName: widget.exercise.audio!,
|
||||||
baseUrl: '${baseUrl}uploads/exercise/audio/',
|
baseUrl: '${baseUrl}api/uploads/exercise/audio/',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ 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/exercise_model.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class MatchingPairsQuestion extends StatelessWidget {
|
class MatchingPairsQuestion extends StatefulWidget {
|
||||||
final dynamic exercise;
|
final dynamic exercise;
|
||||||
final bool isReview;
|
final bool isReview;
|
||||||
|
|
||||||
|
|
@ -17,108 +17,155 @@ class MatchingPairsQuestion extends StatelessWidget {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<MatchingPairsQuestion> createState() => _MatchingPairsQuestionState();
|
||||||
final provider = Provider.of<ExerciseProvider>(context);
|
}
|
||||||
List<dynamic> pairs = [];
|
|
||||||
Map<String, String> studentAnswers = {};
|
|
||||||
final currentIndex = provider.currentExerciseIndex;
|
|
||||||
|
|
||||||
if (exercise is ExerciseModel) {
|
class _MatchingPairsQuestionState extends State<MatchingPairsQuestion> {
|
||||||
pairs = (exercise.choices as MatchingPair).pairs;
|
late List<dynamic> pairs;
|
||||||
} else if (exercise is ReviewExerciseDetail) {
|
late List<String> originalRightPairs;
|
||||||
if (exercise.matchingPairs != null) {
|
late List<String> shuffledRightPairs;
|
||||||
pairs = exercise.matchingPairs!.map((reviewPair) {
|
late Map<String, String> studentAnswers;
|
||||||
|
late Map<String, String> originalPairMapping;
|
||||||
|
late Map<String, int> originalPairIndices;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
initializePairs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// void initializePairs() {
|
||||||
|
// pairs = [];
|
||||||
|
// studentAnswers = {};
|
||||||
|
// originalPairMapping = {};
|
||||||
|
// originalPairIndices = {};
|
||||||
|
|
||||||
|
// if (widget.exercise is ExerciseModel) {
|
||||||
|
// // Mode Exercise
|
||||||
|
// pairs = (widget.exercise.choices as MatchingPair).pairs;
|
||||||
|
// final provider = Provider.of<ExerciseProvider>(context, listen: false);
|
||||||
|
// final currentIndex = provider.currentExerciseIndex;
|
||||||
|
|
||||||
|
// // Simpan indeks asli untuk setiap pasangan kiri
|
||||||
|
// for (int i = 0; i < pairs.length; i++) {
|
||||||
|
// originalPairIndices[pairs[i].left] = i;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// originalRightPairs = pairs.map((pair) => pair.right as String).toList();
|
||||||
|
// shuffledRightPairs =
|
||||||
|
// List<String>.from(provider.shuffledRightPairsList[currentIndex]);
|
||||||
|
// } else if (widget.exercise is ReviewExerciseDetail) {
|
||||||
|
// // Mode Review
|
||||||
|
// if (widget.exercise.matchingPairs != null) {
|
||||||
|
// if (widget.exercise.answerStudent.isNotEmpty) {
|
||||||
|
// final answerPairs = widget.exercise.answerStudent.split(', ');
|
||||||
|
|
||||||
|
// // Buat pairs berdasarkan urutan jawaban siswa
|
||||||
|
// pairs = answerPairs.map((pair) {
|
||||||
|
// final parts = pair.split('-');
|
||||||
|
// return Pair(left: parts[0], right: parts[1]);
|
||||||
|
// }).toList();
|
||||||
|
|
||||||
|
// // Simpan indeks asli dan mapping untuk setiap pasangan
|
||||||
|
// for (int i = 0; i < pairs.length; i++) {
|
||||||
|
// final left = pairs[i].left;
|
||||||
|
// final right = pairs[i].right;
|
||||||
|
// originalPairIndices[left] = i;
|
||||||
|
// studentAnswers[left] = right;
|
||||||
|
// originalPairMapping[left] = right;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Set originalRightPairs dan shuffledRightPairs sama dengan urutan jawaban siswa
|
||||||
|
// originalRightPairs =
|
||||||
|
// pairs.map((pair) => pair.right as String).toList();
|
||||||
|
// shuffledRightPairs =
|
||||||
|
// originalRightPairs.toList(); // Tidak diacak untuk review
|
||||||
|
// } else {
|
||||||
|
// // Jika tidak ada jawaban, gunakan data asli
|
||||||
|
// pairs = widget.exercise.matchingPairs!.map((reviewPair) {
|
||||||
|
// return Pair(
|
||||||
|
// left: reviewPair.leftPair,
|
||||||
|
// right: reviewPair.rightPair,
|
||||||
|
// );
|
||||||
|
// }).toList();
|
||||||
|
|
||||||
|
// // Simpan indeks asli untuk setiap pasangan
|
||||||
|
// for (int i = 0; i < pairs.length; i++) {
|
||||||
|
// originalPairIndices[pairs[i].left] = i;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// originalRightPairs =
|
||||||
|
// pairs.map((pair) => pair.right as String).toList();
|
||||||
|
// shuffledRightPairs = originalRightPairs.toList();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Bangun pemetaan konsisten
|
||||||
|
// for (int i = 0; i < pairs.length; i++) {
|
||||||
|
// final left = pairs[i].left;
|
||||||
|
// final right = pairs[i].right;
|
||||||
|
// originalPairMapping[left] = right;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
void initializePairs() {
|
||||||
|
pairs = [];
|
||||||
|
studentAnswers = {};
|
||||||
|
originalPairMapping = {};
|
||||||
|
originalPairIndices = {};
|
||||||
|
|
||||||
|
if (widget.exercise is ExerciseModel) {
|
||||||
|
// Mode Exercise
|
||||||
|
pairs = (widget.exercise.choices as MatchingPair).pairs;
|
||||||
|
final provider = Provider.of<ExerciseProvider>(context, listen: false);
|
||||||
|
final currentIndex = provider.currentExerciseIndex;
|
||||||
|
|
||||||
|
// Simpan indeks asli untuk setiap pasangan kiri
|
||||||
|
for (int i = 0; i < pairs.length; i++) {
|
||||||
|
originalPairIndices[pairs[i].left] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
originalRightPairs = pairs.map((pair) => pair.right as String).toList();
|
||||||
|
shuffledRightPairs =
|
||||||
|
List<String>.from(provider.shuffledRightPairsList[currentIndex]);
|
||||||
|
} else if (widget.exercise is ReviewExerciseDetail) {
|
||||||
|
// Mode Review
|
||||||
|
if (widget.exercise.matchingPairs != null) {
|
||||||
|
// Gunakan data asli dari exercise untuk mempertahankan urutan
|
||||||
|
pairs = widget.exercise.matchingPairs!.map((reviewPair) {
|
||||||
return Pair(
|
return Pair(
|
||||||
left: reviewPair.leftPair,
|
left: reviewPair.leftPair,
|
||||||
right: reviewPair.rightPair,
|
right: reviewPair.rightPair,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
// Parse student answers
|
// Simpan indeks asli untuk setiap pasangan
|
||||||
if (exercise.answerStudent.isNotEmpty) {
|
for (int i = 0; i < pairs.length; i++) {
|
||||||
final answerPairs = exercise.answerStudent.split(', ');
|
originalPairIndices[pairs[i].left] = i;
|
||||||
for (var pair in answerPairs) {
|
}
|
||||||
final parts = pair.split('-');
|
|
||||||
if (parts.length == 2) {
|
// Use the original right pairs without shuffling
|
||||||
studentAnswers[parts[0]] = parts[1];
|
originalRightPairs = pairs.map((pair) => pair.right as String).toList();
|
||||||
}
|
shuffledRightPairs =
|
||||||
}
|
originalRightPairs.toList(); // No shuffling for review
|
||||||
|
|
||||||
|
// Rekonstruksi jawaban siswa
|
||||||
|
if (widget.exercise.answerStudent.isNotEmpty) {
|
||||||
|
final answerPairs = widget.exercise.answerStudent.split(', ');
|
||||||
|
studentAnswers = {
|
||||||
|
for (var pair in answerPairs) pair.split('-')[0]: pair.split('-')[1]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a reverse mapping for easier lookup of left pair from right pair
|
// Bangun pemetaan konsisten
|
||||||
Map<String, String> reverseStudentAnswers = {};
|
for (int i = 0; i < pairs.length; i++) {
|
||||||
studentAnswers.forEach((left, right) {
|
final left = pairs[i].left;
|
||||||
reverseStudentAnswers[right] = left;
|
final right = pairs[i].right;
|
||||||
});
|
originalPairMapping[left] = right;
|
||||||
|
}
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
...pairs.asMap().entries.map((entry) {
|
|
||||||
int pairIndex = entry.key;
|
|
||||||
dynamic pair = entry.value;
|
|
||||||
String left = pair is Pair ? pair.left : pair.leftPair;
|
|
||||||
String right = pair is Pair ? pair.right : pair.rightPair;
|
|
||||||
|
|
||||||
// Get the corresponding color for this pair
|
|
||||||
Color pairColor = _getPairColor(pairIndex);
|
|
||||||
|
|
||||||
// Find if this item is matched in student answers
|
|
||||||
String? studentMatchedRight = studentAnswers[left];
|
|
||||||
String? matchedLeftForRight = reverseStudentAnswers[right];
|
|
||||||
|
|
||||||
// Find the color index for the matched pair
|
|
||||||
int? matchedPairIndex;
|
|
||||||
if (isReview && matchedLeftForRight != null) {
|
|
||||||
matchedPairIndex = pairs.indexWhere((p) {
|
|
||||||
if (p is Pair) {
|
|
||||||
return p.left == matchedLeftForRight;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildOptionItem(
|
|
||||||
context,
|
|
||||||
left,
|
|
||||||
provider,
|
|
||||||
currentIndex,
|
|
||||||
pairIndex,
|
|
||||||
true,
|
|
||||||
studentMatchedRight != null,
|
|
||||||
studentAnswers,
|
|
||||||
pairColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: _buildOptionItem(
|
|
||||||
context,
|
|
||||||
right,
|
|
||||||
provider,
|
|
||||||
currentIndex,
|
|
||||||
matchedPairIndex ?? pairIndex,
|
|
||||||
false,
|
|
||||||
matchedLeftForRight != null,
|
|
||||||
studentAnswers,
|
|
||||||
matchedPairIndex != null
|
|
||||||
? _getPairColor(matchedPairIndex)
|
|
||||||
: pairColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Color _getPairColor(int index) {
|
Color _getPairColor(int index) {
|
||||||
|
|
@ -137,33 +184,84 @@ class MatchingPairsQuestion extends StatelessWidget {
|
||||||
String option,
|
String option,
|
||||||
ExerciseProvider provider,
|
ExerciseProvider provider,
|
||||||
int exerciseIndex,
|
int exerciseIndex,
|
||||||
int pairIndex,
|
|
||||||
bool isLeft,
|
bool isLeft,
|
||||||
bool isMatched,
|
|
||||||
Map<String, String> studentAnswers,
|
Map<String, String> studentAnswers,
|
||||||
Color pairColor,
|
|
||||||
) {
|
) {
|
||||||
Color? color;
|
|
||||||
bool isSelected = false;
|
bool isSelected = false;
|
||||||
bool isActive = false;
|
bool isActive = false;
|
||||||
|
Color? color;
|
||||||
|
|
||||||
if (isReview) {
|
// Mencari indeks asli pasangan untuk warna yang konsisten
|
||||||
if (isMatched) {
|
int originalPairIndex = -1;
|
||||||
color = pairColor;
|
|
||||||
isSelected = true;
|
if (isLeft) {
|
||||||
|
originalPairIndex = pairs.indexWhere((pair) =>
|
||||||
|
pair is Pair ? pair.left == option : pair.leftPair == option);
|
||||||
|
isActive = provider.activeLeftOption == option;
|
||||||
|
} else {
|
||||||
|
// Untuk right pair, cari berdasarkan pasangan yang sudah terbentuk
|
||||||
|
if (widget.isReview) {
|
||||||
|
// Untuk mode review, periksa apakah opsi ini ada di jawaban siswa
|
||||||
|
final matchedLeft = studentAnswers.keys.firstWhere(
|
||||||
|
(key) => studentAnswers[key] == option,
|
||||||
|
orElse: () => '',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchedLeft.isNotEmpty) {
|
||||||
|
originalPairIndex = pairs.indexWhere((pair) => pair is Pair
|
||||||
|
? pair.left == matchedLeft
|
||||||
|
: pair.leftPair == matchedLeft);
|
||||||
|
isSelected = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (provider.hasMatchedPair(exerciseIndex, option)) {
|
||||||
|
String? matchedLeft =
|
||||||
|
provider.getMatchedLeftPair(exerciseIndex, option);
|
||||||
|
if (matchedLeft != null) {
|
||||||
|
originalPairIndex = pairs.indexWhere((pair) => pair is Pair
|
||||||
|
? pair.left == matchedLeft
|
||||||
|
: pair.leftPair == matchedLeft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menentukan warna berdasarkan status
|
||||||
|
if (widget.isReview) {
|
||||||
|
if (isLeft) {
|
||||||
|
// Untuk left pair di mode review
|
||||||
|
final matchedRight = studentAnswers[option];
|
||||||
|
if (matchedRight != null) {
|
||||||
|
isSelected = true;
|
||||||
|
// Cari indeks pasangan untuk warna yang konsisten
|
||||||
|
originalPairIndex = pairs.indexWhere(
|
||||||
|
(pair) => (pair is Pair ? pair.left : pair.leftPair) == option);
|
||||||
|
color = _getPairColor(originalPairIndex);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Untuk right pair di mode review
|
||||||
|
final matchedLeft = studentAnswers.keys.firstWhere(
|
||||||
|
(key) => studentAnswers[key] == option,
|
||||||
|
orElse: () => '',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchedLeft.isNotEmpty) {
|
||||||
|
isSelected = true;
|
||||||
|
// Cari indeks pasangan untuk warna yang konsisten
|
||||||
|
originalPairIndex = pairs.indexWhere((pair) =>
|
||||||
|
(pair is Pair ? pair.left : pair.leftPair) == matchedLeft);
|
||||||
|
color = _getPairColor(originalPairIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isLeft) {
|
|
||||||
color = provider.getLeftColor(exerciseIndex, pairIndex);
|
|
||||||
isActive = provider.activeLeftOption == option;
|
|
||||||
} else {
|
|
||||||
color = provider.getRightColor(exerciseIndex, pairIndex);
|
|
||||||
}
|
|
||||||
isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft);
|
isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft);
|
||||||
|
if (isSelected || isActive) {
|
||||||
|
color = _getPairColor(originalPairIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: isReview
|
onTap: widget.isReview
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
if (!isLeft && provider.activeLeftOption == null) {
|
if (!isLeft && provider.activeLeftOption == null) {
|
||||||
|
|
@ -179,8 +277,10 @@ class MatchingPairsQuestion extends StatelessWidget {
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? color ?? pairColor
|
? color ?? _getPairColor(originalPairIndex)
|
||||||
: (isActive ? pairColor : AppColors.whiteColor),
|
: (isActive
|
||||||
|
? _getPairColor(originalPairIndex)
|
||||||
|
: 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),
|
||||||
|
|
@ -192,10 +292,10 @@ class MatchingPairsQuestion extends StatelessWidget {
|
||||||
: AppColors.cardDisabledColor,
|
: AppColors.cardDisabledColor,
|
||||||
width: isSelected ? 2 : 1,
|
width: isSelected ? 2 : 1,
|
||||||
),
|
),
|
||||||
boxShadow: isActive && !isReview
|
boxShadow: isActive && !widget.isReview
|
||||||
? [
|
? [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: pairColor.withOpacity(0.5),
|
color: _getPairColor(originalPairIndex).withOpacity(0.5),
|
||||||
spreadRadius: 1,
|
spreadRadius: 1,
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
|
|
@ -216,4 +316,52 @@ class MatchingPairsQuestion extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = Provider.of<ExerciseProvider>(context);
|
||||||
|
final currentIndex = provider.currentExerciseIndex;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
...pairs.asMap().entries.map((entry) {
|
||||||
|
final int pairIndex = entry.key;
|
||||||
|
final dynamic pair = entry.value;
|
||||||
|
final String left = pair is Pair ? pair.left : pair.leftPair;
|
||||||
|
// Menggunakan shuffledRightPairs untuk right pair
|
||||||
|
final String right = shuffledRightPairs[pairIndex];
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildOptionItem(
|
||||||
|
context,
|
||||||
|
left,
|
||||||
|
provider,
|
||||||
|
currentIndex,
|
||||||
|
true,
|
||||||
|
studentAnswers,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: _buildOptionItem(
|
||||||
|
context,
|
||||||
|
right,
|
||||||
|
provider,
|
||||||
|
currentIndex,
|
||||||
|
false,
|
||||||
|
studentAnswers,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@ class Section {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final String description;
|
final String description;
|
||||||
final String thumbnail;
|
final String? thumbnail;
|
||||||
final DateTime timeSection;
|
final DateTime timeSection;
|
||||||
|
|
||||||
Section({
|
Section({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.thumbnail,
|
this.thumbnail,
|
||||||
required this.timeSection,
|
required this.timeSection,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
|
||||||
if (thumbnail.startsWith('http')) {
|
if (thumbnail.startsWith('http')) {
|
||||||
return thumbnail;
|
return thumbnail;
|
||||||
} else {
|
} else {
|
||||||
return '${baseUrl}uploads/section/$thumbnail';
|
return '${baseUrl}api/uploads/section/$thumbnail';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,7 +84,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
Image.network(
|
Image.network(
|
||||||
_getFullImageUrl(selectedSection.thumbnail),
|
_getFullImageUrl(selectedSection.thumbnail ?? ''),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 115,
|
height: 115,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ class SectionProvider extends ChangeNotifier {
|
||||||
final SectionRepository _repository = SectionRepository();
|
final SectionRepository _repository = SectionRepository();
|
||||||
List<Section> _sections = [];
|
List<Section> _sections = [];
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _error;
|
dynamic _error;
|
||||||
|
|
||||||
List<Section> get sections => _sections;
|
List<Section> get sections => _sections;
|
||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,28 @@ class _LearningScreenState extends State<LearningScreen> {
|
||||||
return _buildShimmerLoading();
|
return _buildShimmerLoading();
|
||||||
} else if (sectionProvider.error != null) {
|
} else if (sectionProvider.error != null) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text('Error: ${sectionProvider.error}'));
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Error: ${sectionProvider.error}',
|
||||||
|
style: AppTextStyles.greyTextStyle,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
// Tambahkan tombol retry jika diperlukan
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _fetchSections,
|
||||||
|
child: Text('Retry'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
} else if (sectionProvider.sections.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
'No sections available',
|
||||||
|
style: AppTextStyles.greyTextStyle,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: sectionProvider.sections.length,
|
itemCount: sectionProvider.sections.length,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class _LearningCardState extends State<LearningCard>
|
||||||
if (thumbnail.startsWith('http')) {
|
if (thumbnail.startsWith('http')) {
|
||||||
return thumbnail;
|
return thumbnail;
|
||||||
} else {
|
} else {
|
||||||
return '${baseUrl}uploads/section/$thumbnail';
|
return '${baseUrl}api/uploads/section/$thumbnail';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@ class _LearningCardState extends State<LearningCard>
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Image.network(
|
child: Image.network(
|
||||||
_getFullImageUrl(widget.section.thumbnail),
|
_getFullImageUrl(widget.section.thumbnail ?? ''),
|
||||||
width: 90,
|
width: 90,
|
||||||
height: 104,
|
height: 104,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||||
child: UserAvatar(
|
child: UserAvatar(
|
||||||
radius: 60,
|
radius: 60,
|
||||||
pictureUrl: userProvider.userData?['PICTURE'],
|
pictureUrl: userProvider.userData?['PICTURE'],
|
||||||
baseUrl: '$baseUrl/uploads/avatar/',
|
baseUrl: '${baseUrl}api/uploads/avatar/',
|
||||||
onImageSelected: (File image) {
|
onImageSelected: (File image) {
|
||||||
userProvider.setSelectedImage(image);
|
userProvider.setSelectedImage(image);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
UserAvatar(
|
UserAvatar(
|
||||||
radius: 60,
|
radius: 60,
|
||||||
pictureUrl: userProvider.userData?['PICTURE'],
|
pictureUrl: userProvider.userData?['PICTURE'],
|
||||||
baseUrl: '$baseUrl/uploads/avatar/',
|
baseUrl: '${baseUrl}api/uploads/avatar/',
|
||||||
onImageSelected: (File image) {
|
onImageSelected: (File image) {
|
||||||
userProvider.setSelectedImage(image);
|
userProvider.setSelectedImage(image);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
BIN
workspace.tar
Normal file
BIN
workspace.tar
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user