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/profile/binding/profile_binding.dart';
|
||||||
import 'package:quiz_app/feature/quiz_creation/binding/quiz_creation_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_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/binding/quiz_preview_binding.dart';
|
||||||
import 'package:quiz_app/feature/quiz_preview/view/quiz_preview.dart';
|
import 'package:quiz_app/feature/quiz_preview/view/quiz_preview.dart';
|
||||||
import 'package:quiz_app/feature/register/binding/register_binding.dart';
|
import 'package:quiz_app/feature/register/binding/register_binding.dart';
|
||||||
|
@ -71,6 +73,11 @@ class AppPages {
|
||||||
name: AppRoutes.detailQuizPage,
|
name: AppRoutes.detailQuizPage,
|
||||||
page: () => DetailQuizView(),
|
page: () => DetailQuizView(),
|
||||||
binding: DetailQuizBinding(),
|
binding: DetailQuizBinding(),
|
||||||
|
),
|
||||||
|
GetPage(
|
||||||
|
name: AppRoutes.playQuizPage,
|
||||||
|
page: () => QuizPlayView(),
|
||||||
|
binding: QuizPlayBinding(),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,4 +12,6 @@ abstract class AppRoutes {
|
||||||
static const quizPreviewPage = "/quiz/preview";
|
static const quizPreviewPage = "/quiz/preview";
|
||||||
|
|
||||||
static const detailQuizPage = "/quiz/detail";
|
static const detailQuizPage = "/quiz/detail";
|
||||||
|
|
||||||
|
static const playQuizPage = "/quiz/play";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:get/get.dart';
|
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';
|
import 'package:quiz_app/data/models/quiz/library_quiz_model.dart';
|
||||||
|
|
||||||
class DetailQuizController extends GetxController {
|
class DetailQuizController extends GetxController {
|
||||||
|
@ -12,4 +13,6 @@ class DetailQuizController extends GetxController {
|
||||||
void loadData() {
|
void loadData() {
|
||||||
data = Get.arguments as QuizData;
|
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),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
GlobalButton(text: "Kerjakan", onPressed: () {}),
|
GlobalButton(text: "Kerjakan", onPressed: controller.goToPlayPage),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
GlobalButton(text: "buat ruangan", onPressed: () {}),
|
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