Updated various files, including constants, exercise providers, question widgets, section models, and screens for topics, learning, and settings, with changes to URLs, error handling, and UI components.
This commit is contained in:
parent
10b2c8010d
commit
7b04d304aa
|
|
@ -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,28 +185,21 @@ 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);
|
||||
// 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});
|
||||
|
||||
_rightColors[exerciseIndex][rightIndex] =
|
||||
_leftColors[exerciseIndex][leftIndex];
|
||||
|
||||
// Reset active left option
|
||||
_activeLeftOption = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _updateMatchingPairAnswer(int exerciseIndex) {
|
||||
if (_matchingAnswers[exerciseIndex] != null &&
|
||||
|
|
@ -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 = {};
|
||||
State<MatchingPairsQuestion> createState() => _MatchingPairsQuestionState();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (exercise is ExerciseModel) {
|
||||
pairs = (exercise.choices as MatchingPair).pairs;
|
||||
} else if (exercise is ReviewExerciseDetail) {
|
||||
if (exercise.matchingPairs != null) {
|
||||
pairs = exercise.matchingPairs!.map((reviewPair) {
|
||||
// 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;
|
||||
// Bangun pemetaan konsisten
|
||||
for (int i = 0; i < pairs.length; i++) {
|
||||
final left = pairs[i].left;
|
||||
final right = pairs[i].right;
|
||||
originalPairMapping[left] = right;
|
||||
}
|
||||
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) {
|
||||
|
|
@ -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;
|
||||
// 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 (isLeft) {
|
||||
color = provider.getLeftColor(exerciseIndex, pairIndex);
|
||||
isActive = provider.activeLeftOption == option;
|
||||
} else {
|
||||
color = provider.getRightColor(exerciseIndex, pairIndex);
|
||||
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 {
|
||||
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