feat: quiz play logic
This commit is contained in:
parent
d4d9f0d85d
commit
b80303b9c0
|
@ -13,6 +13,8 @@ import 'package:quiz_app/feature/navigation/views/navbar_view.dart';
|
|||
import 'package:quiz_app/feature/profile/binding/profile_binding.dart';
|
||||
import 'package:quiz_app/feature/quiz_creation/binding/quiz_creation_binding.dart';
|
||||
import 'package:quiz_app/feature/quiz_creation/view/quiz_creation_view.dart';
|
||||
import 'package:quiz_app/feature/quiz_play/binding/quiz_play_binding.dart';
|
||||
import 'package:quiz_app/feature/quiz_play/view/quiz_play_view.dart';
|
||||
import 'package:quiz_app/feature/quiz_preview/binding/quiz_preview_binding.dart';
|
||||
import 'package:quiz_app/feature/quiz_preview/view/quiz_preview.dart';
|
||||
import 'package:quiz_app/feature/register/binding/register_binding.dart';
|
||||
|
@ -71,6 +73,11 @@ class AppPages {
|
|||
name: AppRoutes.detailQuizPage,
|
||||
page: () => DetailQuizView(),
|
||||
binding: DetailQuizBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: AppRoutes.playQuizPage,
|
||||
page: () => QuizPlayView(),
|
||||
binding: QuizPlayBinding(),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
|
|
@ -12,4 +12,6 @@ abstract class AppRoutes {
|
|||
static const quizPreviewPage = "/quiz/preview";
|
||||
|
||||
static const detailQuizPage = "/quiz/detail";
|
||||
|
||||
static const playQuizPage = "/quiz/play";
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:quiz_app/app/routes/app_pages.dart';
|
||||
import 'package:quiz_app/data/models/quiz/library_quiz_model.dart';
|
||||
|
||||
class DetailQuizController extends GetxController {
|
||||
|
@ -12,4 +13,6 @@ class DetailQuizController extends GetxController {
|
|||
void loadData() {
|
||||
data = Get.arguments as QuizData;
|
||||
}
|
||||
|
||||
void goToPlayPage() => Get.toNamed(AppRoutes.playQuizPage, arguments: data);
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ class DetailQuizView extends GetView<DetailQuizController> {
|
|||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
GlobalButton(text: "Kerjakan", onPressed: () {}),
|
||||
GlobalButton(text: "Kerjakan", onPressed: controller.goToPlayPage),
|
||||
const SizedBox(height: 20),
|
||||
GlobalButton(text: "buat ruangan", onPressed: () {}),
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:quiz_app/feature/quiz_play/controller/quiz_play_controller.dart';
|
||||
|
||||
class QuizPlayBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<QuizPlayController>(() => QuizPlayController());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:quiz_app/core/utils/logger.dart';
|
||||
import 'package:quiz_app/data/models/quiz/library_quiz_model.dart';
|
||||
import 'package:quiz_app/data/models/quiz/question_listings_model.dart';
|
||||
|
||||
class QuizPlayController extends GetxController {
|
||||
late final QuizData quizData;
|
||||
|
||||
final currentIndex = 0.obs;
|
||||
final timeLeft = 0.obs;
|
||||
final isStarting = true.obs;
|
||||
final isAnswerSelected = false.obs;
|
||||
final selectedAnswer = ''.obs;
|
||||
final List<AnsweredQuestion> answeredQuestions = [];
|
||||
|
||||
final answerTextController = TextEditingController();
|
||||
final choosenAnswerTOF = 0.obs;
|
||||
Timer? _timer;
|
||||
|
||||
QuestionListing get currentQuestion => quizData.questionListings[currentIndex.value];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
quizData = Get.arguments as QuizData;
|
||||
_startCountdown();
|
||||
|
||||
// Listener untuk fill the blank
|
||||
answerTextController.addListener(() {
|
||||
if (answerTextController.text.trim().isNotEmpty) {
|
||||
isAnswerSelected.value = true;
|
||||
} else {
|
||||
isAnswerSelected.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Listener untuk pilihan true/false
|
||||
ever(choosenAnswerTOF, (value) {
|
||||
if (value != 0) {
|
||||
isAnswerSelected.value = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _startCountdown() async {
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
isStarting.value = false;
|
||||
_startTimer();
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
timeLeft.value = currentQuestion.duration;
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (timeLeft.value > 0) {
|
||||
timeLeft.value--;
|
||||
} else {
|
||||
_submitAnswerIfNeeded();
|
||||
_nextQuestion();
|
||||
}
|
||||
});
|
||||
isAnswerSelected.value = false;
|
||||
}
|
||||
|
||||
void selectAnswer(String answer) {
|
||||
selectedAnswer.value = answer;
|
||||
isAnswerSelected.value = true;
|
||||
}
|
||||
|
||||
void onChooseTOF(bool value) {
|
||||
choosenAnswerTOF.value = value ? 1 : 2;
|
||||
selectedAnswer.value = value ? "Ya" : "Tidak";
|
||||
}
|
||||
|
||||
void _submitAnswerIfNeeded() {
|
||||
final question = currentQuestion;
|
||||
String userAnswer = "";
|
||||
|
||||
if (question.type == "fill_the_blank") {
|
||||
userAnswer = answerTextController.text.trim();
|
||||
} else if (question.type == "option") {
|
||||
userAnswer = selectedAnswer.value.trim();
|
||||
} else if (question.type == "true_false") {
|
||||
userAnswer = selectedAnswer.value.trim();
|
||||
}
|
||||
|
||||
// Masukkan ke answeredQuestions
|
||||
answeredQuestions.add(AnsweredQuestion(
|
||||
index: currentIndex.value,
|
||||
question: question.question,
|
||||
selectedAnswer: userAnswer,
|
||||
correctAnswer: question.targetAnswer.trim(),
|
||||
isCorrect: userAnswer.toLowerCase() == question.targetAnswer.trim().toLowerCase(),
|
||||
));
|
||||
}
|
||||
|
||||
void nextQuestion() {
|
||||
_submitAnswerIfNeeded();
|
||||
_nextQuestion();
|
||||
}
|
||||
|
||||
void _nextQuestion() {
|
||||
_timer?.cancel();
|
||||
if (currentIndex.value < quizData.questionListings.length - 1) {
|
||||
currentIndex.value++;
|
||||
answerTextController.clear();
|
||||
selectedAnswer.value = '';
|
||||
choosenAnswerTOF.value = 0;
|
||||
_startTimer();
|
||||
} else {
|
||||
_finishQuiz();
|
||||
}
|
||||
}
|
||||
|
||||
void _finishQuiz() {
|
||||
_timer?.cancel();
|
||||
logC.i(answeredQuestions.map((e) => e.toJson()).toList());
|
||||
Get.defaultDialog(
|
||||
title: "Selesai",
|
||||
middleText: "Kamu telah menyelesaikan semua soal!",
|
||||
onConfirm: () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
},
|
||||
textConfirm: "OK",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_timer?.cancel();
|
||||
answerTextController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
class AnsweredQuestion {
|
||||
final int index;
|
||||
final String question;
|
||||
final String selectedAnswer;
|
||||
final String correctAnswer;
|
||||
final bool isCorrect;
|
||||
|
||||
AnsweredQuestion({
|
||||
required this.index,
|
||||
required this.question,
|
||||
required this.selectedAnswer,
|
||||
required this.correctAnswer,
|
||||
required this.isCorrect,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'index': index,
|
||||
'question': question,
|
||||
'selectedAnswer': selectedAnswer,
|
||||
'correctAnswer': correctAnswer,
|
||||
'isCorrect': isCorrect,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:quiz_app/component/global_text_field.dart';
|
||||
import 'package:quiz_app/feature/quiz_play/controller/quiz_play_controller.dart';
|
||||
|
||||
class QuizPlayView extends GetView<QuizPlayController> {
|
||||
const QuizPlayView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF9FAFB),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
title: const Text(
|
||||
'Kerjakan Soal',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Obx(() {
|
||||
final question = controller.currentQuestion;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LinearProgressIndicator(
|
||||
value: controller.timeLeft.value / question.duration,
|
||||
minHeight: 8,
|
||||
backgroundColor: Colors.grey[300],
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF2563EB)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Soal ${controller.currentIndex.value + 1} dari ${controller.quizData.questionListings.length}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
question.question,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Jawaban Berdasarkan Tipe Soal
|
||||
if (question.type == 'option' && question.options != null)
|
||||
...List.generate(question.options!.length, (index) {
|
||||
final option = question.options![index];
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
side: const BorderSide(color: Colors.grey),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () => controller.selectAnswer(option),
|
||||
child: Text(option),
|
||||
),
|
||||
);
|
||||
})
|
||||
else if (question.type == 'true_false')
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildTrueFalseButton('Ya', true, controller.choosenAnswerTOF),
|
||||
_buildTrueFalseButton('Tidak', false, controller.choosenAnswerTOF),
|
||||
],
|
||||
)
|
||||
else
|
||||
GlobalTextField(controller: controller.answerTextController),
|
||||
const Spacer(),
|
||||
Obx(() {
|
||||
return ElevatedButton(
|
||||
onPressed: controller.nextQuestion,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: controller.isAnswerSelected.value ? const Color(0xFF2563EB) : Colors.grey,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Next',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTrueFalseButton(String label, bool value, RxInt choosenAnswer) {
|
||||
return Obx(() {
|
||||
bool isSelected = (choosenAnswer.value == (value ? 1 : 2));
|
||||
return ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isSelected ? (value ? Colors.green[100] : Colors.red[100]) : Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
side: const BorderSide(color: Colors.grey),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () => controller.onChooseTOF(value),
|
||||
icon: Icon(value ? Icons.check_circle_outline : Icons.cancel_outlined),
|
||||
label: Text(label),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue