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() {
|
||||
_dio.options.baseUrl = baseUrl;
|
||||
_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 {
|
||||
|
|
|
|||
|
|
@ -439,8 +439,7 @@ class _HomeContentState extends State<HomeContent> {
|
|||
),
|
||||
completedTopicsProvider.isLoading
|
||||
? _buildShimmerEffect()
|
||||
: completedTopicsProvider.completedTopics == null ||
|
||||
completedTopicsProvider.completedTopics.isEmpty
|
||||
: completedTopicsProvider.completedTopics.isEmpty
|
||||
? _buildNoDataWidget()
|
||||
: _buildCompletedTopicsContent(
|
||||
completedTopicsProvider),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class ProgressCard extends StatelessWidget {
|
|||
String _getFullImageUrl(String thumbnail) {
|
||||
return thumbnail.startsWith('http')
|
||||
? thumbnail
|
||||
: '${baseUrl}uploads/section/$thumbnail';
|
||||
: '${baseUrl}api/uploads/section/$thumbnail';
|
||||
}
|
||||
|
||||
Future<void> _navigateToTopics(
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
String _nameLevel = '';
|
||||
String? _activeLeftOption;
|
||||
String? _studentLearningId;
|
||||
List<List<dynamic>> _shuffledRightPairsList = [];
|
||||
List<List<dynamic>> _savedShuffledRightPairsList = [];
|
||||
|
||||
// Constants
|
||||
final List<Color> _pairColors = [
|
||||
|
|
@ -47,6 +49,9 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
String? get activeLeftOption => _activeLeftOption;
|
||||
String? get studentLearningId => _studentLearningId;
|
||||
List<ReviewExerciseDetail> get reviewExercises => _reviewExercises;
|
||||
List<List<dynamic>> get shuffledRightPairsList => _shuffledRightPairsList;
|
||||
List<List<dynamic>> get savedShuffledRightPairsList =>
|
||||
_savedShuffledRightPairsList;
|
||||
|
||||
// Initialization methods
|
||||
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
|
||||
// void answerQuestion(int index, String answer) {
|
||||
// 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) {
|
||||
final matchingPair = _exercises[exerciseIndex].choices as MatchingPair;
|
||||
final isLeft = matchingPair.pairs.any((pair) => pair.left == option);
|
||||
|
|
@ -137,27 +185,20 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
return;
|
||||
}
|
||||
|
||||
final pairs = (_exercises[exerciseIndex].choices as MatchingPair).pairs;
|
||||
final leftIndex =
|
||||
pairs.indexWhere((pair) => pair.left == _activeLeftOption);
|
||||
final rightIndex = pairs.indexWhere((pair) => pair.right == option);
|
||||
|
||||
if (leftIndex != -1 && rightIndex != -1) {
|
||||
if (!_matchingAnswers.containsKey(exerciseIndex)) {
|
||||
_matchingAnswers[exerciseIndex] = [];
|
||||
}
|
||||
|
||||
_matchingAnswers[exerciseIndex]!.removeWhere((pair) =>
|
||||
pair['left'] == _activeLeftOption || pair['right'] == option);
|
||||
|
||||
_matchingAnswers[exerciseIndex]!
|
||||
.add({'left': _activeLeftOption!, 'right': option});
|
||||
|
||||
_rightColors[exerciseIndex][rightIndex] =
|
||||
_leftColors[exerciseIndex][leftIndex];
|
||||
|
||||
_activeLeftOption = null;
|
||||
if (!_matchingAnswers.containsKey(exerciseIndex)) {
|
||||
_matchingAnswers[exerciseIndex] = [];
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
|
@ -260,11 +301,16 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
|
||||
// API methods
|
||||
Future<void> fetchExercises(String levelId) async {
|
||||
if (_isLoading) return; // Hindari multiple calls
|
||||
|
||||
_isLoading = true;
|
||||
_currentExerciseIndex = 0;
|
||||
_resetReviewState();
|
||||
notifyListeners();
|
||||
|
||||
// Reset shuffled pairs
|
||||
_shuffledRightPairsList = [];
|
||||
|
||||
try {
|
||||
final token = await _userProvider.getValidToken();
|
||||
if (token == null) {
|
||||
|
|
@ -275,9 +321,16 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
_nameTopic = data['NAME_TOPIC'];
|
||||
_nameLevel = data['NAME_LEVEL'];
|
||||
final exercisesData = data['EXERCISES'];
|
||||
|
||||
// Map exercises
|
||||
_exercises = exercisesData
|
||||
.map<ExerciseModel>((json) => ExerciseModel.fromJson(json))
|
||||
.toList();
|
||||
|
||||
// Inisialisasi pengacakan untuk matching pairs
|
||||
initializeShuffledRightPairs(_exercises);
|
||||
|
||||
// Inisialisasi jawaban
|
||||
_answers = List.generate(_exercises.length, (index) => '');
|
||||
initializeAnswers();
|
||||
} catch (e) {
|
||||
|
|
@ -305,6 +358,8 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
final payload = response['payload'];
|
||||
_reviewData = ReviewExerciseModel.fromJson(payload);
|
||||
|
||||
restoreShuffledRightPairs();
|
||||
|
||||
// Sort the exercises based on the title number
|
||||
_reviewExercises = (_reviewData?.stdExercises ?? [])
|
||||
..sort((a, b) {
|
||||
|
|
@ -376,7 +431,8 @@ class ExerciseProvider extends ChangeNotifier {
|
|||
}
|
||||
|
||||
Future<Map<String, dynamic>> submitAnswersAndGetScore() async {
|
||||
print('submitAnswersAndGetScore called');
|
||||
// Simpan urutan acak sebelum submit
|
||||
saveShuffledRightPairs();
|
||||
try {
|
||||
if (_studentLearningId == null) {
|
||||
throw Exception('Student Learning ID is not set');
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ class _ExerciseContentState extends State<ExerciseContent>
|
|||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: ImageWidget(
|
||||
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(
|
||||
key: _audioPlayerKey,
|
||||
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:provider/provider.dart';
|
||||
|
||||
class MatchingPairsQuestion extends StatelessWidget {
|
||||
class MatchingPairsQuestion extends StatefulWidget {
|
||||
final dynamic exercise;
|
||||
final bool isReview;
|
||||
|
||||
|
|
@ -17,108 +17,155 @@ class MatchingPairsQuestion extends StatelessWidget {
|
|||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<ExerciseProvider>(context);
|
||||
List<dynamic> pairs = [];
|
||||
Map<String, String> studentAnswers = {};
|
||||
final currentIndex = provider.currentExerciseIndex;
|
||||
State<MatchingPairsQuestion> createState() => _MatchingPairsQuestionState();
|
||||
}
|
||||
|
||||
if (exercise is ExerciseModel) {
|
||||
pairs = (exercise.choices as MatchingPair).pairs;
|
||||
} else if (exercise is ReviewExerciseDetail) {
|
||||
if (exercise.matchingPairs != null) {
|
||||
pairs = exercise.matchingPairs!.map((reviewPair) {
|
||||
class _MatchingPairsQuestionState extends State<MatchingPairsQuestion> {
|
||||
late List<dynamic> pairs;
|
||||
late List<String> originalRightPairs;
|
||||
late List<String> shuffledRightPairs;
|
||||
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(
|
||||
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];
|
||||
}
|
||||
}
|
||||
// Simpan indeks asli untuk setiap pasangan
|
||||
for (int i = 0; i < pairs.length; i++) {
|
||||
originalPairIndices[pairs[i].left] = i;
|
||||
}
|
||||
|
||||
// Use the original right pairs without shuffling
|
||||
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
|
||||
Map<String, String> reverseStudentAnswers = {};
|
||||
studentAnswers.forEach((left, right) {
|
||||
reverseStudentAnswers[right] = left;
|
||||
});
|
||||
|
||||
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(),
|
||||
],
|
||||
);
|
||||
// Bangun pemetaan konsisten
|
||||
for (int i = 0; i < pairs.length; i++) {
|
||||
final left = pairs[i].left;
|
||||
final right = pairs[i].right;
|
||||
originalPairMapping[left] = right;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getPairColor(int index) {
|
||||
|
|
@ -137,33 +184,84 @@ class MatchingPairsQuestion extends StatelessWidget {
|
|||
String option,
|
||||
ExerciseProvider provider,
|
||||
int exerciseIndex,
|
||||
int pairIndex,
|
||||
bool isLeft,
|
||||
bool isMatched,
|
||||
Map<String, String> studentAnswers,
|
||||
Color pairColor,
|
||||
) {
|
||||
Color? color;
|
||||
bool isSelected = false;
|
||||
bool isActive = false;
|
||||
Color? color;
|
||||
|
||||
if (isReview) {
|
||||
if (isMatched) {
|
||||
color = pairColor;
|
||||
isSelected = true;
|
||||
// Mencari indeks asli pasangan untuk warna yang konsisten
|
||||
int originalPairIndex = -1;
|
||||
|
||||
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 {
|
||||
if (isLeft) {
|
||||
color = provider.getLeftColor(exerciseIndex, pairIndex);
|
||||
isActive = provider.activeLeftOption == option;
|
||||
} else {
|
||||
color = provider.getRightColor(exerciseIndex, pairIndex);
|
||||
}
|
||||
isSelected = provider.isOptionSelected(exerciseIndex, option, isLeft);
|
||||
if (isSelected || isActive) {
|
||||
color = _getPairColor(originalPairIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: isReview
|
||||
onTap: widget.isReview
|
||||
? null
|
||||
: () {
|
||||
if (!isLeft && provider.activeLeftOption == null) {
|
||||
|
|
@ -179,8 +277,10 @@ class MatchingPairsQuestion extends StatelessWidget {
|
|||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? color ?? pairColor
|
||||
: (isActive ? pairColor : AppColors.whiteColor),
|
||||
? color ?? _getPairColor(originalPairIndex)
|
||||
: (isActive
|
||||
? _getPairColor(originalPairIndex)
|
||||
: AppColors.whiteColor),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(20),
|
||||
bottomRight: Radius.circular(20),
|
||||
|
|
@ -192,10 +292,10 @@ class MatchingPairsQuestion extends StatelessWidget {
|
|||
: AppColors.cardDisabledColor,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
boxShadow: isActive && !isReview
|
||||
boxShadow: isActive && !widget.isReview
|
||||
? [
|
||||
BoxShadow(
|
||||
color: pairColor.withOpacity(0.5),
|
||||
color: _getPairColor(originalPairIndex).withOpacity(0.5),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 4,
|
||||
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 name;
|
||||
final String description;
|
||||
final String thumbnail;
|
||||
final String? thumbnail;
|
||||
final DateTime timeSection;
|
||||
|
||||
Section({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.thumbnail,
|
||||
this.thumbnail,
|
||||
required this.timeSection,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
|
|||
if (thumbnail.startsWith('http')) {
|
||||
return thumbnail;
|
||||
} else {
|
||||
return '${baseUrl}uploads/section/$thumbnail';
|
||||
return '${baseUrl}api/uploads/section/$thumbnail';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ class _TopicsListScreenState extends State<TopicsListScreen> {
|
|||
Stack(
|
||||
children: [
|
||||
Image.network(
|
||||
_getFullImageUrl(selectedSection.thumbnail),
|
||||
_getFullImageUrl(selectedSection.thumbnail ?? ''),
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: 115,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class SectionProvider extends ChangeNotifier {
|
|||
final SectionRepository _repository = SectionRepository();
|
||||
List<Section> _sections = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
dynamic _error;
|
||||
|
||||
List<Section> get sections => _sections;
|
||||
bool get isLoading => _isLoading;
|
||||
|
|
|
|||
|
|
@ -70,7 +70,28 @@ class _LearningScreenState extends State<LearningScreen> {
|
|||
return _buildShimmerLoading();
|
||||
} else if (sectionProvider.error != null) {
|
||||
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 {
|
||||
return ListView.builder(
|
||||
itemCount: sectionProvider.sections.length,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class _LearningCardState extends State<LearningCard>
|
|||
if (thumbnail.startsWith('http')) {
|
||||
return thumbnail;
|
||||
} else {
|
||||
return '${baseUrl}uploads/section/$thumbnail';
|
||||
return '${baseUrl}api/uploads/section/$thumbnail';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ class _LearningCardState extends State<LearningCard>
|
|||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
_getFullImageUrl(widget.section.thumbnail),
|
||||
_getFullImageUrl(widget.section.thumbnail ?? ''),
|
||||
width: 90,
|
||||
height: 104,
|
||||
fit: BoxFit.cover,
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||
child: UserAvatar(
|
||||
radius: 60,
|
||||
pictureUrl: userProvider.userData?['PICTURE'],
|
||||
baseUrl: '$baseUrl/uploads/avatar/',
|
||||
baseUrl: '${baseUrl}api/uploads/avatar/',
|
||||
onImageSelected: (File image) {
|
||||
userProvider.setSelectedImage(image);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
UserAvatar(
|
||||
radius: 60,
|
||||
pictureUrl: userProvider.userData?['PICTURE'],
|
||||
baseUrl: '$baseUrl/uploads/avatar/',
|
||||
baseUrl: '${baseUrl}api/uploads/avatar/',
|
||||
onImageSelected: (File 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