fix: issue on the play quiz controller
This commit is contained in:
parent
81d900878f
commit
82f4a1ec41
|
@ -119,9 +119,9 @@ class SocketService {
|
|||
});
|
||||
}
|
||||
|
||||
void startQuiz({required String sessionCode}) {
|
||||
void startQuiz({required String sessionId}) {
|
||||
socket.emit('start_quiz', {
|
||||
'session_code': sessionCode,
|
||||
'session_id': sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -129,6 +129,7 @@ class SocketService {
|
|||
required String sessionId,
|
||||
required String userId,
|
||||
required int questionIndex,
|
||||
required int timeSpent,
|
||||
required dynamic answer,
|
||||
}) {
|
||||
socket.emit('submit_answer', {
|
||||
|
@ -136,6 +137,7 @@ class SocketService {
|
|||
'user_id': userId,
|
||||
'question_index': questionIndex,
|
||||
'answer': answer,
|
||||
'time_spent': timeSpent,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -46,33 +46,58 @@ class MonitorQuizController extends GetxController {
|
|||
_socketService.scoreUpdates.listen((data) {
|
||||
logC.i("📊 Score Update Received: $data");
|
||||
|
||||
final Map<String, dynamic> scoreMap = Map<String, dynamic>.from(data);
|
||||
// Ensure data is a valid map
|
||||
// if (data is! Map) {
|
||||
// logC.e("Invalid score update format: $data");
|
||||
// return;
|
||||
// }
|
||||
|
||||
scoreMap.forEach((userId, scoreData) {
|
||||
// Parse the score data more carefully
|
||||
final List<dynamic> scoreList = data['scores'] ?? [];
|
||||
|
||||
for (var scoreData in scoreList) {
|
||||
// Safely extract user ID and score information
|
||||
final String? userId = scoreData['user_id'];
|
||||
|
||||
if (userId == null) {
|
||||
logC.w("Skipping score update with missing user ID");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the index of the participant
|
||||
final index = participan.indexWhere((p) => p.id == userId);
|
||||
|
||||
if (index != -1) {
|
||||
// Participant found, update their scores
|
||||
final participant = participan[index];
|
||||
final correct = scoreData["correct"] ?? 0;
|
||||
final incorrect = scoreData["incorrect"] ?? 0;
|
||||
|
||||
// Safely extract correct and incorrect values, default to 0
|
||||
final int correct = scoreData['correct'] ?? 0;
|
||||
final int incorrect = scoreData['incorrect'] ?? 0;
|
||||
final int totalScore = scoreData['total_score'] ?? 0;
|
||||
|
||||
// Update participant scores
|
||||
participant.correct.value = correct;
|
||||
participant.wrong.value = incorrect;
|
||||
participant.totalScore.value = totalScore; // Assuming you have a totalScore observable
|
||||
} else {
|
||||
// Participant not found, optionally add new participant
|
||||
// Participant not found, add new participant
|
||||
participan.add(
|
||||
ParticipantAnswerPoint(
|
||||
id: userId,
|
||||
name: "Unknown", // Or fetch proper name if available
|
||||
correct: (scoreData["correct"] ?? 0).obs,
|
||||
wrong: (scoreData["incorrect"] ?? 0).obs,
|
||||
name: "Unknown", // Consider fetching proper name if possible
|
||||
correct: (scoreData['correct'] ?? 0).obs,
|
||||
wrong: (scoreData['incorrect'] ?? 0).obs,
|
||||
totalScore: (scoreData['total_score'] ?? 0).obs,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Notify observers if needed (optional)
|
||||
// Sort participants by total score (optional)
|
||||
participan.sort((a, b) => b.totalScore.value.compareTo(a.totalScore.value));
|
||||
|
||||
// Notify observers
|
||||
participan.refresh();
|
||||
});
|
||||
}
|
||||
|
@ -83,12 +108,15 @@ class ParticipantAnswerPoint {
|
|||
final String name;
|
||||
final RxInt correct;
|
||||
final RxInt wrong;
|
||||
final RxInt totalScore;
|
||||
|
||||
ParticipantAnswerPoint({
|
||||
required this.id,
|
||||
required this.name,
|
||||
RxInt? correct,
|
||||
RxInt? wrong,
|
||||
RxInt? totalScore,
|
||||
}) : correct = correct ?? 0.obs,
|
||||
wrong = wrong ?? 0.obs;
|
||||
wrong = wrong ?? 0.obs,
|
||||
totalScore = totalScore ?? 0.obs;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:quiz_app/component/global_button.dart';
|
||||
|
@ -7,21 +8,21 @@ import 'package:quiz_app/data/services/socket_service.dart';
|
|||
class PlayQuizMultiplayerController extends GetxController {
|
||||
final SocketService _socketService;
|
||||
final UserController _userController;
|
||||
|
||||
PlayQuizMultiplayerController(this._socketService, this._userController);
|
||||
|
||||
// final questions = <MultiplayerQuestionModel>[].obs;
|
||||
final Rxn<MultiplayerQuestionModel> currentQuestion = Rxn<MultiplayerQuestionModel>();
|
||||
final currentQuestionIndex = 0.obs;
|
||||
final selectedAnswer = Rxn<String>();
|
||||
final isDone = false.obs;
|
||||
|
||||
final Rx<ButtonType> buttonType = ButtonType.disabled.obs;
|
||||
|
||||
final fillInAnswerController = TextEditingController();
|
||||
RxBool isASentAns = false.obs;
|
||||
|
||||
late final String sessionCode;
|
||||
// Timer related variables
|
||||
final RxInt remainingTime = 0.obs;
|
||||
Timer? _timer;
|
||||
|
||||
late final String sessionId;
|
||||
late final bool isAdmin;
|
||||
|
||||
@override
|
||||
|
@ -33,14 +34,13 @@ class PlayQuizMultiplayerController extends GetxController {
|
|||
|
||||
_loadData() {
|
||||
final args = Get.arguments as Map<String, dynamic>;
|
||||
sessionCode = args["session_code"];
|
||||
sessionId = args["session_id"];
|
||||
isAdmin = args["is_admin"];
|
||||
}
|
||||
|
||||
_registerListener() {
|
||||
fillInAnswerController.addListener(() {
|
||||
final text = fillInAnswerController.text;
|
||||
|
||||
if (text.isNotEmpty) {
|
||||
buttonType.value = ButtonType.primary;
|
||||
} else {
|
||||
|
@ -52,17 +52,52 @@ class PlayQuizMultiplayerController extends GetxController {
|
|||
buttonType.value = ButtonType.disabled;
|
||||
fillInAnswerController.clear();
|
||||
isASentAns.value = false;
|
||||
selectedAnswer.value = null;
|
||||
|
||||
final model = MultiplayerQuestionModel.fromJson(Map<String, dynamic>.from(data));
|
||||
currentQuestion.value = model;
|
||||
fillInAnswerController.clear();
|
||||
|
||||
// Start the timer for this question
|
||||
_startTimer(model.duration);
|
||||
});
|
||||
|
||||
_socketService.quizDone.listen((_) {
|
||||
isDone.value = true;
|
||||
// Cancel timer when quiz is done
|
||||
_cancelTimer();
|
||||
});
|
||||
}
|
||||
|
||||
// Start timer with the question duration
|
||||
void _startTimer(int duration) {
|
||||
// Cancel any existing timer
|
||||
_cancelTimer();
|
||||
|
||||
// Set initial remaining time in seconds
|
||||
remainingTime.value = duration;
|
||||
|
||||
// Create a timer that ticks every second
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (remainingTime.value > 0) {
|
||||
remainingTime.value--;
|
||||
} else {
|
||||
// Time's up - cancel the timer
|
||||
_cancelTimer();
|
||||
|
||||
// Auto-submit if the user hasn't already submitted an answer
|
||||
if (!isASentAns.value) {
|
||||
submitAnswer();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cancel the timer
|
||||
void _cancelTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
void selectOptionAnswer(String option) {
|
||||
selectedAnswer.value = option;
|
||||
buttonType.value = ButtonType.primary;
|
||||
|
@ -76,8 +111,8 @@ class PlayQuizMultiplayerController extends GetxController {
|
|||
void submitAnswer() {
|
||||
final question = currentQuestion.value!;
|
||||
final type = question.type;
|
||||
|
||||
String? answer;
|
||||
|
||||
if (type == 'fill_the_blank') {
|
||||
answer = fillInAnswerController.text.trim();
|
||||
} else {
|
||||
|
@ -86,10 +121,11 @@ class PlayQuizMultiplayerController extends GetxController {
|
|||
|
||||
if (answer != null && answer.isNotEmpty) {
|
||||
_socketService.sendAnswer(
|
||||
sessionId: sessionCode,
|
||||
sessionId: sessionId,
|
||||
userId: _userController.userData!.id,
|
||||
questionIndex: question.questionIndex,
|
||||
answer: answer,
|
||||
timeSpent: question.duration - remainingTime.value,
|
||||
);
|
||||
isASentAns.value = true;
|
||||
}
|
||||
|
@ -98,6 +134,7 @@ class PlayQuizMultiplayerController extends GetxController {
|
|||
@override
|
||||
void onClose() {
|
||||
fillInAnswerController.dispose();
|
||||
_cancelTimer(); // Important: cancel timer when controller is closed
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,15 +9,7 @@ class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF9FAFB),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
"Soal ${(controller.currentQuestion.value?.questionIndex ?? 0)}/10",
|
||||
style: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
// Remove the AppBar and put everything in the body
|
||||
body: Obx(() {
|
||||
if (controller.isDone.value) {
|
||||
return _buildDoneView();
|
||||
|
@ -37,27 +29,110 @@ class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
|
|||
|
||||
Widget _buildQuestionView() {
|
||||
final question = controller.currentQuestion.value!;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
question.question,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (question.type == 'option') _buildOptionQuestion(),
|
||||
if (question.type == 'fill_the_blank') _buildFillInBlankQuestion(),
|
||||
if (question.type == 'true_false') _buildTrueFalseQuestion(),
|
||||
const Spacer(),
|
||||
Obx(
|
||||
() => GlobalButton(
|
||||
text: "Kirim jawaban",
|
||||
onPressed: controller.submitAnswer,
|
||||
type: controller.buttonType.value,
|
||||
// Custom AppBar content moved to body
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Back button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
// Title
|
||||
Text(
|
||||
"Soal ${(question.questionIndex + 1)}/10",
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
// Empty container for spacing
|
||||
Container(width: 48),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
|
||||
// Timer progress bar
|
||||
Obx(() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Time remaining text
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Waktu tersisa:",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${controller.remainingTime.value} detik",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: controller.remainingTime.value <= 10 ? Colors.red : const Color(0xFF2563EB),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Progress bar
|
||||
LinearProgressIndicator(
|
||||
value: controller.remainingTime.value / question.duration,
|
||||
minHeight: 8,
|
||||
backgroundColor: Colors.grey[300],
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
controller.remainingTime.value <= 10 ? Colors.red : const Color(0xFF2563EB),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Question content
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
question.question,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (question.type == 'option') _buildOptionQuestion(),
|
||||
if (question.type == 'fill_the_blank') _buildFillInBlankQuestion(),
|
||||
if (question.type == 'true_false') _buildTrueFalseQuestion(),
|
||||
const Spacer(),
|
||||
Obx(
|
||||
() => GlobalButton(
|
||||
text: "Kirim jawaban",
|
||||
onPressed: controller.submitAnswer,
|
||||
type: controller.buttonType.value,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -129,40 +204,45 @@ class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
|
|||
}
|
||||
|
||||
Widget _buildDoneView() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
"Kuis telah selesai!",
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Arahkan ke halaman hasil atau leaderboard
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2563EB),
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
size: 80,
|
||||
color: Color(0xFF2563EB),
|
||||
),
|
||||
child: const Text("Lihat Hasil", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
"Kuis telah selesai!",
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Terima kasih telah berpartisipasi.",
|
||||
style: TextStyle(fontSize: 16, color: Colors.black54),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Arahkan ke halaman hasil atau leaderboard
|
||||
Get.back();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2563EB),
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: const Text("Lihat Hasil", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget _buildProgressBar() {
|
||||
// final question = controller.currentQuestion;
|
||||
// return LinearProgressIndicator(
|
||||
// value: controller.timeLeft.value / question.duration,
|
||||
// minHeight: 8,
|
||||
// backgroundColor: Colors.grey[300],
|
||||
// valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF2563EB)),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ class WaitingRoomController extends GetxController {
|
|||
WaitingRoomController(this._socketService, this._userController);
|
||||
|
||||
final sessionCode = ''.obs;
|
||||
String sessionId = '';
|
||||
final quizMeta = Rx<QuizInfo?>(null);
|
||||
final joinedUsers = <UserModel>[].obs;
|
||||
final isAdmin = true.obs;
|
||||
|
@ -38,6 +39,7 @@ class WaitingRoomController extends GetxController {
|
|||
isAdmin.value = data.isAdmin;
|
||||
|
||||
sessionCode.value = roomData!.sessionCode;
|
||||
sessionId = roomData!.sessionId;
|
||||
|
||||
quizMeta.value = data.quizInfo;
|
||||
|
||||
|
@ -85,7 +87,7 @@ class WaitingRoomController extends GetxController {
|
|||
Get.snackbar("Info", "Kuis telah dimulai");
|
||||
if (!isAdmin.value) {
|
||||
Get.offAllNamed(AppRoutes.playQuizMPLPage, arguments: {
|
||||
"session_code": sessionCode.value,
|
||||
"session_id": sessionId,
|
||||
"is_admin": isAdmin.value,
|
||||
});
|
||||
}
|
||||
|
@ -104,9 +106,9 @@ class WaitingRoomController extends GetxController {
|
|||
}
|
||||
|
||||
void startQuiz() {
|
||||
_socketService.startQuiz(sessionCode: sessionCode.value);
|
||||
_socketService.startQuiz(sessionId: sessionId);
|
||||
Get.offAllNamed(AppRoutes.monitorQuizMPLPage, arguments: {
|
||||
"session_code": sessionCode.value,
|
||||
"session_id": sessionId,
|
||||
"is_admin": isAdmin.value,
|
||||
"list_participan": joinedUsers.toList(),
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue