develop #1

Merged
akhdanre merged 104 commits from develop into main 2025-07-10 12:38:53 +07:00
5 changed files with 233 additions and 84 deletions
Showing only changes of commit 82f4a1ec41 - Show all commits

View File

@ -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,
});
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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)),
// );
// }
}

View File

@ -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(),
});