fix: issue on the play quiz controller

This commit is contained in:
akhdanre 2025-05-17 20:57:25 +07:00
parent 81d900878f
commit 82f4a1ec41
5 changed files with 233 additions and 84 deletions

View File

@ -119,9 +119,9 @@ class SocketService {
}); });
} }
void startQuiz({required String sessionCode}) { void startQuiz({required String sessionId}) {
socket.emit('start_quiz', { socket.emit('start_quiz', {
'session_code': sessionCode, 'session_id': sessionId,
}); });
} }
@ -129,6 +129,7 @@ class SocketService {
required String sessionId, required String sessionId,
required String userId, required String userId,
required int questionIndex, required int questionIndex,
required int timeSpent,
required dynamic answer, required dynamic answer,
}) { }) {
socket.emit('submit_answer', { socket.emit('submit_answer', {
@ -136,6 +137,7 @@ class SocketService {
'user_id': userId, 'user_id': userId,
'question_index': questionIndex, 'question_index': questionIndex,
'answer': answer, 'answer': answer,
'time_spent': timeSpent,
}); });
} }

View File

@ -46,33 +46,58 @@ class MonitorQuizController extends GetxController {
_socketService.scoreUpdates.listen((data) { _socketService.scoreUpdates.listen((data) {
logC.i("📊 Score Update Received: $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); final index = participan.indexWhere((p) => p.id == userId);
if (index != -1) { if (index != -1) {
// Participant found, update their scores // Participant found, update their scores
final participant = participan[index]; 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.correct.value = correct;
participant.wrong.value = incorrect; participant.wrong.value = incorrect;
participant.totalScore.value = totalScore; // Assuming you have a totalScore observable
} else { } else {
// Participant not found, optionally add new participant // Participant not found, add new participant
participan.add( participan.add(
ParticipantAnswerPoint( ParticipantAnswerPoint(
id: userId, id: userId,
name: "Unknown", // Or fetch proper name if available name: "Unknown", // Consider fetching proper name if possible
correct: (scoreData["correct"] ?? 0).obs, correct: (scoreData['correct'] ?? 0).obs,
wrong: (scoreData["incorrect"] ?? 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(); participan.refresh();
}); });
} }
@ -83,12 +108,15 @@ class ParticipantAnswerPoint {
final String name; final String name;
final RxInt correct; final RxInt correct;
final RxInt wrong; final RxInt wrong;
final RxInt totalScore;
ParticipantAnswerPoint({ ParticipantAnswerPoint({
required this.id, required this.id,
required this.name, required this.name,
RxInt? correct, RxInt? correct,
RxInt? wrong, RxInt? wrong,
RxInt? totalScore,
}) : correct = correct ?? 0.obs, }) : correct = correct ?? 0.obs,
wrong = wrong ?? 0.obs; wrong = wrong ?? 0.obs,
totalScore = totalScore ?? 0.obs;
} }

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/component/global_button.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 { class PlayQuizMultiplayerController extends GetxController {
final SocketService _socketService; final SocketService _socketService;
final UserController _userController; final UserController _userController;
PlayQuizMultiplayerController(this._socketService, this._userController); PlayQuizMultiplayerController(this._socketService, this._userController);
// final questions = <MultiplayerQuestionModel>[].obs;
final Rxn<MultiplayerQuestionModel> currentQuestion = Rxn<MultiplayerQuestionModel>(); final Rxn<MultiplayerQuestionModel> currentQuestion = Rxn<MultiplayerQuestionModel>();
final currentQuestionIndex = 0.obs; final currentQuestionIndex = 0.obs;
final selectedAnswer = Rxn<String>(); final selectedAnswer = Rxn<String>();
final isDone = false.obs; final isDone = false.obs;
final Rx<ButtonType> buttonType = ButtonType.disabled.obs; final Rx<ButtonType> buttonType = ButtonType.disabled.obs;
final fillInAnswerController = TextEditingController(); final fillInAnswerController = TextEditingController();
RxBool isASentAns = false.obs; 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; late final bool isAdmin;
@override @override
@ -33,14 +34,13 @@ class PlayQuizMultiplayerController extends GetxController {
_loadData() { _loadData() {
final args = Get.arguments as Map<String, dynamic>; final args = Get.arguments as Map<String, dynamic>;
sessionCode = args["session_code"]; sessionId = args["session_id"];
isAdmin = args["is_admin"]; isAdmin = args["is_admin"];
} }
_registerListener() { _registerListener() {
fillInAnswerController.addListener(() { fillInAnswerController.addListener(() {
final text = fillInAnswerController.text; final text = fillInAnswerController.text;
if (text.isNotEmpty) { if (text.isNotEmpty) {
buttonType.value = ButtonType.primary; buttonType.value = ButtonType.primary;
} else { } else {
@ -52,17 +52,52 @@ class PlayQuizMultiplayerController extends GetxController {
buttonType.value = ButtonType.disabled; buttonType.value = ButtonType.disabled;
fillInAnswerController.clear(); fillInAnswerController.clear();
isASentAns.value = false; isASentAns.value = false;
selectedAnswer.value = null;
final model = MultiplayerQuestionModel.fromJson(Map<String, dynamic>.from(data)); final model = MultiplayerQuestionModel.fromJson(Map<String, dynamic>.from(data));
currentQuestion.value = model; currentQuestion.value = model;
fillInAnswerController.clear();
// Start the timer for this question
_startTimer(model.duration);
}); });
_socketService.quizDone.listen((_) { _socketService.quizDone.listen((_) {
isDone.value = true; 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) { void selectOptionAnswer(String option) {
selectedAnswer.value = option; selectedAnswer.value = option;
buttonType.value = ButtonType.primary; buttonType.value = ButtonType.primary;
@ -76,8 +111,8 @@ class PlayQuizMultiplayerController extends GetxController {
void submitAnswer() { void submitAnswer() {
final question = currentQuestion.value!; final question = currentQuestion.value!;
final type = question.type; final type = question.type;
String? answer; String? answer;
if (type == 'fill_the_blank') { if (type == 'fill_the_blank') {
answer = fillInAnswerController.text.trim(); answer = fillInAnswerController.text.trim();
} else { } else {
@ -86,10 +121,11 @@ class PlayQuizMultiplayerController extends GetxController {
if (answer != null && answer.isNotEmpty) { if (answer != null && answer.isNotEmpty) {
_socketService.sendAnswer( _socketService.sendAnswer(
sessionId: sessionCode, sessionId: sessionId,
userId: _userController.userData!.id, userId: _userController.userData!.id,
questionIndex: question.questionIndex, questionIndex: question.questionIndex,
answer: answer, answer: answer,
timeSpent: question.duration - remainingTime.value,
); );
isASentAns.value = true; isASentAns.value = true;
} }
@ -98,6 +134,7 @@ class PlayQuizMultiplayerController extends GetxController {
@override @override
void onClose() { void onClose() {
fillInAnswerController.dispose(); fillInAnswerController.dispose();
_cancelTimer(); // Important: cancel timer when controller is closed
super.onClose(); super.onClose();
} }
} }

View File

@ -9,15 +9,7 @@ class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF9FAFB), backgroundColor: const Color(0xFFF9FAFB),
appBar: AppBar( // Remove the AppBar and put everything in the body
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),
),
),
body: Obx(() { body: Obx(() {
if (controller.isDone.value) { if (controller.isDone.value) {
return _buildDoneView(); return _buildDoneView();
@ -37,7 +29,86 @@ class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
Widget _buildQuestionView() { Widget _buildQuestionView() {
final question = controller.currentQuestion.value!; final question = controller.currentQuestion.value!;
return SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 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( 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), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -60,6 +131,10 @@ class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
) )
], ],
), ),
),
),
],
),
); );
} }
@ -129,19 +204,33 @@ class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
} }
Widget _buildDoneView() { Widget _buildDoneView() {
return Padding( return SafeArea(
child: Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(
Icons.check_circle,
size: 80,
color: Color(0xFF2563EB),
),
const SizedBox(height: 20),
const Text( const Text(
"Kuis telah selesai!", "Kuis telah selesai!",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 16), 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( ElevatedButton(
onPressed: () { onPressed: () {
// Arahkan ke halaman hasil atau leaderboard // Arahkan ke halaman hasil atau leaderboard
Get.back();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2563EB), backgroundColor: const Color(0xFF2563EB),
@ -153,16 +242,7 @@ class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
), ),
], ],
), ),
),
); );
} }
// 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)),
// );
// }
} }

View File

@ -16,6 +16,7 @@ class WaitingRoomController extends GetxController {
WaitingRoomController(this._socketService, this._userController); WaitingRoomController(this._socketService, this._userController);
final sessionCode = ''.obs; final sessionCode = ''.obs;
String sessionId = '';
final quizMeta = Rx<QuizInfo?>(null); final quizMeta = Rx<QuizInfo?>(null);
final joinedUsers = <UserModel>[].obs; final joinedUsers = <UserModel>[].obs;
final isAdmin = true.obs; final isAdmin = true.obs;
@ -38,6 +39,7 @@ class WaitingRoomController extends GetxController {
isAdmin.value = data.isAdmin; isAdmin.value = data.isAdmin;
sessionCode.value = roomData!.sessionCode; sessionCode.value = roomData!.sessionCode;
sessionId = roomData!.sessionId;
quizMeta.value = data.quizInfo; quizMeta.value = data.quizInfo;
@ -85,7 +87,7 @@ class WaitingRoomController extends GetxController {
Get.snackbar("Info", "Kuis telah dimulai"); Get.snackbar("Info", "Kuis telah dimulai");
if (!isAdmin.value) { if (!isAdmin.value) {
Get.offAllNamed(AppRoutes.playQuizMPLPage, arguments: { Get.offAllNamed(AppRoutes.playQuizMPLPage, arguments: {
"session_code": sessionCode.value, "session_id": sessionId,
"is_admin": isAdmin.value, "is_admin": isAdmin.value,
}); });
} }
@ -104,9 +106,9 @@ class WaitingRoomController extends GetxController {
} }
void startQuiz() { void startQuiz() {
_socketService.startQuiz(sessionCode: sessionCode.value); _socketService.startQuiz(sessionId: sessionId);
Get.offAllNamed(AppRoutes.monitorQuizMPLPage, arguments: { Get.offAllNamed(AppRoutes.monitorQuizMPLPage, arguments: {
"session_code": sessionCode.value, "session_id": sessionId,
"is_admin": isAdmin.value, "is_admin": isAdmin.value,
"list_participan": joinedUsers.toList(), "list_participan": joinedUsers.toList(),
}); });