feat: play quiz multiplayer done
This commit is contained in:
parent
7e126e24a6
commit
edb7ab0fdf
|
@ -14,8 +14,11 @@ import 'package:quiz_app/feature/listing_quiz/binding/listing_quiz_binding.dart'
|
||||||
import 'package:quiz_app/feature/listing_quiz/view/listing_quiz_view.dart';
|
import 'package:quiz_app/feature/listing_quiz/view/listing_quiz_view.dart';
|
||||||
import 'package:quiz_app/feature/login/bindings/login_binding.dart';
|
import 'package:quiz_app/feature/login/bindings/login_binding.dart';
|
||||||
import 'package:quiz_app/feature/login/view/login_page.dart';
|
import 'package:quiz_app/feature/login/view/login_page.dart';
|
||||||
|
import 'package:quiz_app/feature/monitor_quiz/view/monitor_quiz_view.dart';
|
||||||
import 'package:quiz_app/feature/navigation/bindings/navigation_binding.dart';
|
import 'package:quiz_app/feature/navigation/bindings/navigation_binding.dart';
|
||||||
import 'package:quiz_app/feature/navigation/views/navbar_view.dart';
|
import 'package:quiz_app/feature/navigation/views/navbar_view.dart';
|
||||||
|
import 'package:quiz_app/feature/play_quiz_multiplayer/binding/play_quiz_multiplayer_binding.dart';
|
||||||
|
import 'package:quiz_app/feature/play_quiz_multiplayer/view/play_quiz_multiplayer.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';
|
||||||
|
@ -120,6 +123,16 @@ class AppPages {
|
||||||
name: AppRoutes.joinRoomPage,
|
name: AppRoutes.joinRoomPage,
|
||||||
page: () => JoinRoomView(),
|
page: () => JoinRoomView(),
|
||||||
binding: JoinRoomBinding(),
|
binding: JoinRoomBinding(),
|
||||||
)
|
),
|
||||||
|
GetPage(
|
||||||
|
name: AppRoutes.monitorQuizMPLPage,
|
||||||
|
page: () => MonitorQuizView(),
|
||||||
|
// binding: JoinRoomBinding(),
|
||||||
|
),
|
||||||
|
GetPage(
|
||||||
|
name: AppRoutes.playQuizMPLPage,
|
||||||
|
page: () => PlayQuizMultiplayerView(),
|
||||||
|
binding: PlayQuizMultiplayerBinding(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,4 +21,7 @@ abstract class AppRoutes {
|
||||||
static const roomPage = "/room/quiz";
|
static const roomPage = "/room/quiz";
|
||||||
static const joinRoomPage = "/room/quiz/join";
|
static const joinRoomPage = "/room/quiz/join";
|
||||||
static const waitRoomPage = "/room/quiz/waiting";
|
static const waitRoomPage = "/room/quiz/waiting";
|
||||||
|
|
||||||
|
static const playQuizMPLPage = "/room/quiz/play";
|
||||||
|
static const monitorQuizMPLPage = "/room/quiz/monitor";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:quiz_app/core/endpoint/api_endpoint.dart';
|
import 'package:quiz_app/core/endpoint/api_endpoint.dart';
|
||||||
|
import 'package:quiz_app/core/utils/logger.dart';
|
||||||
import 'package:socket_io_client/socket_io_client.dart' as io;
|
import 'package:socket_io_client/socket_io_client.dart' as io;
|
||||||
|
|
||||||
class SocketService {
|
class SocketService {
|
||||||
|
@ -7,47 +8,51 @@ class SocketService {
|
||||||
|
|
||||||
final _roomMessageController = StreamController<Map<String, dynamic>>.broadcast();
|
final _roomMessageController = StreamController<Map<String, dynamic>>.broadcast();
|
||||||
final _chatMessageController = StreamController<Map<String, dynamic>>.broadcast();
|
final _chatMessageController = StreamController<Map<String, dynamic>>.broadcast();
|
||||||
|
final _quizStartedController = StreamController<void>.broadcast();
|
||||||
final _errorController = StreamController<String>.broadcast();
|
final _errorController = StreamController<String>.broadcast();
|
||||||
|
|
||||||
Stream<Map<String, dynamic>> get roomMessages => _roomMessageController.stream;
|
Stream<Map<String, dynamic>> get roomMessages => _roomMessageController.stream;
|
||||||
Stream<Map<String, dynamic>> get chatMessages => _chatMessageController.stream;
|
Stream<Map<String, dynamic>> get chatMessages => _chatMessageController.stream;
|
||||||
|
Stream<void> get quizStarted => _quizStartedController.stream;
|
||||||
Stream<String> get errors => _errorController.stream;
|
Stream<String> get errors => _errorController.stream;
|
||||||
|
|
||||||
void initSocketConnection() {
|
void initSocketConnection() {
|
||||||
socket = io.io(
|
socket = io.io(
|
||||||
APIEndpoint.baseUrl,
|
APIEndpoint.baseUrl,
|
||||||
io.OptionBuilder()
|
io.OptionBuilder().setTransports(['websocket']).disableAutoConnect().build(),
|
||||||
.setTransports(['websocket']) // WebSocket mode
|
|
||||||
.disableAutoConnect()
|
|
||||||
.build(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
socket.connect();
|
socket.connect();
|
||||||
|
|
||||||
socket.onConnect((_) {
|
socket.onConnect((_) {
|
||||||
print('Connected: ${socket.id}');
|
logC.i('Connected: ${socket.id}');
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.onDisconnect((_) {
|
socket.onDisconnect((_) {
|
||||||
print('Disconnected');
|
logC.i('Disconnected');
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('connection_response', (data) {
|
socket.on('connection_response', (data) {
|
||||||
print('Connection response: $data');
|
logC.i('Connection response: $data');
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('room_message', (data) {
|
socket.on('room_message', (data) {
|
||||||
print('Room Message: $data');
|
logC.i('Room Message: $data');
|
||||||
_roomMessageController.add(Map<String, dynamic>.from(data));
|
_roomMessageController.add(Map<String, dynamic>.from(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('receive_message', (data) {
|
socket.on('receive_message', (data) {
|
||||||
print('Message from ${data['from']}: ${data['message']}');
|
logC.i('Message from ${data['from']}: ${data['message']}');
|
||||||
_chatMessageController.add(Map<String, dynamic>.from(data));
|
_chatMessageController.add(Map<String, dynamic>.from(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('quiz_started', (_) {
|
||||||
|
logC.i('Quiz has started!');
|
||||||
|
_quizStartedController.add(null);
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('error', (data) {
|
socket.on('error', (data) {
|
||||||
print('Socket error: $data');
|
logC.i('Socket error: $data');
|
||||||
_errorController.add(data.toString());
|
_errorController.add(data.toString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -78,10 +83,44 @@ class SocketService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emit when admin starts the quiz
|
||||||
|
void startQuiz({required String sessionCode}) {
|
||||||
|
socket.emit('start_quiz', {
|
||||||
|
'session_code': sessionCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit user's answer during quiz
|
||||||
|
void sendAnswer({
|
||||||
|
required String sessionId,
|
||||||
|
required String userId,
|
||||||
|
required int questionIndex,
|
||||||
|
required dynamic answer,
|
||||||
|
}) {
|
||||||
|
socket.emit('submit_answer', {
|
||||||
|
'session_id': sessionId,
|
||||||
|
'user_id': userId,
|
||||||
|
'question_index': questionIndex,
|
||||||
|
'answer': answer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit when user finishes the quiz
|
||||||
|
void doneQuiz({
|
||||||
|
required String sessionId,
|
||||||
|
required String userId,
|
||||||
|
}) {
|
||||||
|
socket.emit('quiz_done', {
|
||||||
|
'session_id': sessionId,
|
||||||
|
'user_id': userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
socket.dispose();
|
socket.dispose();
|
||||||
_roomMessageController.close();
|
_roomMessageController.close();
|
||||||
_chatMessageController.close();
|
_chatMessageController.close();
|
||||||
|
_quizStartedController.close();
|
||||||
_errorController.close();
|
_errorController.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MonitorQuizView extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Text("monitor quiz admin"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:quiz_app/data/controllers/user_controller.dart';
|
||||||
|
import 'package:quiz_app/data/services/socket_service.dart';
|
||||||
|
import 'package:quiz_app/feature/play_quiz_multiplayer/controller/play_quiz_controller.dart';
|
||||||
|
|
||||||
|
class PlayQuizMultiplayerBinding extends Bindings {
|
||||||
|
@override
|
||||||
|
void dependencies() {
|
||||||
|
Get.lazyPut(() => PlayQuizMultiplayerController(Get.find<SocketService>(), Get.find<UserController>()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:quiz_app/data/controllers/user_controller.dart';
|
||||||
|
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;
|
||||||
|
|
||||||
|
late final String sessionCode;
|
||||||
|
late final bool isAdmin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
|
||||||
|
final args = Get.arguments as Map<String, dynamic>;
|
||||||
|
sessionCode = args["session_code"];
|
||||||
|
isAdmin = args["is_admin"];
|
||||||
|
|
||||||
|
_socketService.socket.on("quiz_question", (data) {
|
||||||
|
final model = MultiplayerQuestionModel.fromJson(Map<String, dynamic>.from(data));
|
||||||
|
currentQuestion.value = model;
|
||||||
|
questions.add(model);
|
||||||
|
});
|
||||||
|
|
||||||
|
_socketService.socket.on("quiz_done", (_) {
|
||||||
|
isDone.value = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void submitAnswer() {
|
||||||
|
final question = questions[currentQuestionIndex.value];
|
||||||
|
final answer = selectedAnswer.value;
|
||||||
|
|
||||||
|
if (answer != null) {
|
||||||
|
_socketService.sendAnswer(
|
||||||
|
sessionId: sessionCode,
|
||||||
|
userId: _userController.userData!.id,
|
||||||
|
questionIndex: question.questionIndex,
|
||||||
|
answer: answer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentQuestionIndex.value < questions.length - 1) {
|
||||||
|
currentQuestionIndex.value++;
|
||||||
|
selectedAnswer.value = null;
|
||||||
|
} else {
|
||||||
|
isDone.value = true;
|
||||||
|
_socketService.doneQuiz(
|
||||||
|
sessionId: sessionCode,
|
||||||
|
userId: _userController.userData!.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiplayerQuestionModel {
|
||||||
|
final int questionIndex;
|
||||||
|
final String question;
|
||||||
|
final List<String> options;
|
||||||
|
|
||||||
|
MultiplayerQuestionModel({
|
||||||
|
required this.questionIndex,
|
||||||
|
required this.question,
|
||||||
|
required this.options,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MultiplayerQuestionModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return MultiplayerQuestionModel(
|
||||||
|
questionIndex: json['question_index'],
|
||||||
|
question: json['question'],
|
||||||
|
options: List<String>.from(json['options']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'question_index': questionIndex,
|
||||||
|
'question': question,
|
||||||
|
'options': options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:quiz_app/feature/play_quiz_multiplayer/controller/play_quiz_controller.dart';
|
||||||
|
|
||||||
|
class PlayQuizMultiplayerView extends GetView<PlayQuizMultiplayerController> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Obx(() {
|
||||||
|
if (controller.questions.isEmpty) {
|
||||||
|
return const Text("Loading...");
|
||||||
|
}
|
||||||
|
return Text("Soal ${controller.currentQuestionIndex.value + 1}/${controller.questions.length}");
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
body: Obx(() {
|
||||||
|
if (controller.isDone.value) {
|
||||||
|
return _buildDoneView();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.questions.isEmpty) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildQuestionView();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQuestionView() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
controller.currentQuestion.value!.question,
|
||||||
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
...controller.currentQuestion.value!.options.map((option) => RadioListTile<String>(
|
||||||
|
title: Text(option),
|
||||||
|
value: option,
|
||||||
|
groupValue: controller.selectedAnswer.value,
|
||||||
|
onChanged: (value) => controller.selectedAnswer.value = value,
|
||||||
|
)),
|
||||||
|
const Spacer(),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: controller.selectedAnswer.value == null ? null : controller.submitAnswer,
|
||||||
|
child: const Text("Kirim Jawaban"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDoneView() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Kuis telah selesai!",
|
||||||
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {},
|
||||||
|
child: const Text("Lihat Hasil"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:quiz_app/app/routes/app_pages.dart';
|
||||||
import 'package:quiz_app/data/controllers/user_controller.dart';
|
import 'package:quiz_app/data/controllers/user_controller.dart';
|
||||||
import 'package:quiz_app/data/dto/waiting_room_dto.dart';
|
import 'package:quiz_app/data/dto/waiting_room_dto.dart';
|
||||||
import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
|
import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
|
||||||
|
@ -16,25 +17,24 @@ class WaitingRoomController extends GetxController {
|
||||||
final sessionCode = ''.obs;
|
final sessionCode = ''.obs;
|
||||||
final quizMeta = Rx<QuizListingModel?>(null);
|
final quizMeta = Rx<QuizListingModel?>(null);
|
||||||
final joinedUsers = <UserModel>[].obs;
|
final joinedUsers = <UserModel>[].obs;
|
||||||
|
|
||||||
final isAdmin = true.obs;
|
final isAdmin = true.obs;
|
||||||
|
|
||||||
|
final quizQuestions = <Map<String, dynamic>>[].obs;
|
||||||
|
final isQuizStarted = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
_loadDummyData();
|
_loadInitialData();
|
||||||
|
_registerSocketListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _loadDummyData() {
|
void _loadInitialData() {
|
||||||
final data = Get.arguments as WaitingRoomDTO;
|
final data = Get.arguments as WaitingRoomDTO;
|
||||||
|
|
||||||
SessionResponseModel? roomData = data.data;
|
SessionResponseModel? roomData = data.data;
|
||||||
isAdmin.value = data.isAdmin;
|
isAdmin.value = data.isAdmin;
|
||||||
|
|
||||||
_socketService.roomMessages.listen((data) {
|
|
||||||
final user = data["data"];
|
|
||||||
joinedUsers.assign(UserModel(id: user['user_id'], name: user['username']));
|
|
||||||
});
|
|
||||||
sessionCode.value = roomData.sessionCode;
|
sessionCode.value = roomData.sessionCode;
|
||||||
|
|
||||||
quizMeta.value = QuizListingModel(
|
quizMeta.value = QuizListingModel(
|
||||||
|
@ -49,6 +49,31 @@ class WaitingRoomController extends GetxController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _registerSocketListeners() {
|
||||||
|
_socketService.roomMessages.listen((data) {
|
||||||
|
final user = data["data"];
|
||||||
|
if (user != null) {
|
||||||
|
joinedUsers.assign(UserModel(id: user['user_id'], name: user['username']));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_socketService.quizStarted.listen((_) {
|
||||||
|
isQuizStarted.value = true;
|
||||||
|
Get.snackbar("Info", "Kuis telah dimulai");
|
||||||
|
if (isAdmin.value) {
|
||||||
|
Get.offAllNamed(AppRoutes.monitorQuizMPLPage, arguments: {
|
||||||
|
"session_code": sessionCode.value,
|
||||||
|
"is_admin": isAdmin.value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Get.offAllNamed(AppRoutes.playQuizMPLPage, arguments: {
|
||||||
|
"session_code": sessionCode.value,
|
||||||
|
"is_admin": isAdmin.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void copySessionCode(BuildContext context) {
|
void copySessionCode(BuildContext context) {
|
||||||
Clipboard.setData(ClipboardData(text: sessionCode.value));
|
Clipboard.setData(ClipboardData(text: sessionCode.value));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
@ -61,6 +86,6 @@ class WaitingRoomController extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
void startQuiz() {
|
void startQuiz() {
|
||||||
print("Mulai kuis dengan session: ${sessionCode.value}");
|
_socketService.startQuiz(sessionCode: sessionCode.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue