develop #1
|
@ -29,6 +29,8 @@ import 'package:quiz_app/feature/room_maker/binding/room_maker_binding.dart';
|
|||
import 'package:quiz_app/feature/room_maker/view/room_maker_view.dart';
|
||||
import 'package:quiz_app/feature/search/binding/search_binding.dart';
|
||||
import 'package:quiz_app/feature/splash_screen/presentation/splash_screen_page.dart';
|
||||
import 'package:quiz_app/feature/waiting_room/binding/waiting_room_binding.dart';
|
||||
import 'package:quiz_app/feature/waiting_room/view/waiting_room_view.dart';
|
||||
|
||||
part 'app_routes.dart';
|
||||
|
||||
|
@ -106,6 +108,11 @@ class AppPages {
|
|||
name: AppRoutes.roomPage,
|
||||
page: () => RoomMakerView(),
|
||||
binding: RoomMakerBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: AppRoutes.waitRoomPage,
|
||||
page: () => WaitingRoomView(),
|
||||
binding: WaitingRoomBinding(),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
|
|
@ -19,4 +19,5 @@ abstract class AppRoutes {
|
|||
static const detailHistoryPage = "/history/detail";
|
||||
|
||||
static const roomPage = "/room/quiz";
|
||||
static const waitRoomPage = "/room/quiz/waiting";
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class APIEndpoint {
|
||||
static const String baseUrl = "http://192.168.1.9:5000/api";
|
||||
static const String baseUrl = "http://192.168.1.9:5000";
|
||||
static const String api = "$baseUrl/api";
|
||||
|
||||
static const String login = "/login";
|
||||
static const String loginGoogle = "/login/google";
|
||||
|
@ -17,4 +18,6 @@ class APIEndpoint {
|
|||
static const String detailHistoryQuiz = "/history/detail";
|
||||
|
||||
static const String subject = "/subject";
|
||||
|
||||
static const String session = "/session";
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import 'package:quiz_app/data/models/session/session_response_model.dart';
|
||||
|
||||
class WaitingRoomDTO {
|
||||
final bool isAdmin;
|
||||
final SessionResponseModel data;
|
||||
|
||||
WaitingRoomDTO(this.isAdmin, this.data);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
class SessionRequestModel {
|
||||
final String quizId;
|
||||
final String hostId;
|
||||
final int limitParticipan;
|
||||
|
||||
SessionRequestModel({
|
||||
required this.quizId,
|
||||
required this.hostId,
|
||||
required this.limitParticipan,
|
||||
});
|
||||
|
||||
factory SessionRequestModel.fromJson(Map<String, dynamic> json) {
|
||||
return SessionRequestModel(
|
||||
quizId: json['quiz_id'],
|
||||
hostId: json['host_id'],
|
||||
limitParticipan: json['limit_participan'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'quiz_id': quizId,
|
||||
'host_id': hostId,
|
||||
'limit_participan': limitParticipan,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
class SessionResponseModel {
|
||||
final String sessionId;
|
||||
final String sessionCode;
|
||||
|
||||
SessionResponseModel({required this.sessionId, required this.sessionCode});
|
||||
|
||||
factory SessionResponseModel.fromJson(Map<String, dynamic> json) {
|
||||
return SessionResponseModel(
|
||||
sessionId: json['session_id'],
|
||||
sessionCode: json['session_code'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'session_id': sessionId,
|
||||
'session_code': sessionCode,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
class UserModel {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
UserModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
});
|
||||
}
|
|
@ -8,7 +8,7 @@ class ApiClient extends GetxService {
|
|||
|
||||
Future<ApiClient> init() async {
|
||||
dio = Dio(BaseOptions(
|
||||
baseUrl: APIEndpoint.baseUrl,
|
||||
baseUrl: APIEndpoint.api,
|
||||
connectTimeout: const Duration(minutes: 3),
|
||||
receiveTimeout: const Duration(minutes: 10),
|
||||
headers: {
|
||||
|
|
|
@ -20,6 +20,7 @@ class AnswerService extends GetxService {
|
|||
APIEndpoint.quizAnswer,
|
||||
data: payload,
|
||||
);
|
||||
return BaseResponseModel(message: "success");
|
||||
} on DioException catch (e) {
|
||||
logC.e('Gagal mengirim jawaban: ${e.response?.data['message'] ?? e.message}');
|
||||
return null;
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:quiz_app/core/endpoint/api_endpoint.dart';
|
||||
import 'package:quiz_app/data/models/base/base_model.dart';
|
||||
import 'package:quiz_app/data/models/session/session_request_model.dart';
|
||||
import 'package:quiz_app/data/models/session/session_response_model.dart';
|
||||
import 'package:quiz_app/data/providers/dio_client.dart';
|
||||
|
||||
class SessionService extends GetxService {
|
||||
late final Dio _dio;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
_dio = Get.find<ApiClient>().dio;
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
Future<BaseResponseModel<SessionResponseModel>?> createSession(SessionRequestModel data) async {
|
||||
try {
|
||||
final response = await _dio.post(APIEndpoint.session, data: {
|
||||
'quiz_id': data.quizId,
|
||||
'host_id': data.hostId,
|
||||
'limit_participan': data.limitParticipan,
|
||||
});
|
||||
if (response.statusCode != 201) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return BaseResponseModel.fromJson(response.data, (e) => SessionResponseModel.fromJson(e));
|
||||
} on DioException catch (e) {
|
||||
print('Error creating session: ${e.response?.data ?? e.message}');
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Unexpected error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import 'dart:async';
|
||||
import 'package:quiz_app/core/endpoint/api_endpoint.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart' as io;
|
||||
|
||||
class SocketService {
|
||||
late io.Socket socket;
|
||||
|
||||
final _roomMessageController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _chatMessageController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _errorController = StreamController<String>.broadcast();
|
||||
|
||||
Stream<Map<String, dynamic>> get roomMessages => _roomMessageController.stream;
|
||||
Stream<Map<String, dynamic>> get chatMessages => _chatMessageController.stream;
|
||||
Stream<String> get errors => _errorController.stream;
|
||||
|
||||
void initSocketConnection() {
|
||||
socket = io.io(
|
||||
APIEndpoint.baseUrl,
|
||||
io.OptionBuilder()
|
||||
.setTransports(['websocket']) // WebSocket mode
|
||||
.disableAutoConnect()
|
||||
.build(),
|
||||
);
|
||||
|
||||
socket.connect();
|
||||
|
||||
socket.onConnect((_) {
|
||||
print('Connected: ${socket.id}');
|
||||
});
|
||||
|
||||
socket.onDisconnect((_) {
|
||||
print('Disconnected');
|
||||
});
|
||||
|
||||
socket.on('connection_response', (data) {
|
||||
print('Connection response: $data');
|
||||
});
|
||||
|
||||
socket.on('room_message', (data) {
|
||||
print('Room Message: $data');
|
||||
_roomMessageController.add(Map<String, dynamic>.from(data));
|
||||
});
|
||||
|
||||
socket.on('receive_message', (data) {
|
||||
print('Message from ${data['from']}: ${data['message']}');
|
||||
_chatMessageController.add(Map<String, dynamic>.from(data));
|
||||
});
|
||||
|
||||
socket.on('error', (data) {
|
||||
print('Socket error: $data');
|
||||
_errorController.add(data.toString());
|
||||
});
|
||||
}
|
||||
|
||||
void joinRoom({required String sessionCode, required String userId}) {
|
||||
socket.emit('join_room', {
|
||||
'session_code': sessionCode,
|
||||
'user_id': userId,
|
||||
});
|
||||
}
|
||||
|
||||
void leaveRoom({required String sessionId, String username = "anonymous"}) {
|
||||
socket.emit('leave_room', {
|
||||
'session_id': sessionId,
|
||||
'username': username,
|
||||
});
|
||||
}
|
||||
|
||||
void sendMessage({
|
||||
required String sessionId,
|
||||
required String message,
|
||||
String username = "anonymous",
|
||||
}) {
|
||||
socket.emit('send_message', {
|
||||
'session_id': sessionId,
|
||||
'message': message,
|
||||
'username': username,
|
||||
});
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
socket.dispose();
|
||||
_roomMessageController.close();
|
||||
_chatMessageController.close();
|
||||
_errorController.close();
|
||||
}
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:quiz_app/data/controllers/user_controller.dart';
|
||||
import 'package:quiz_app/data/services/session_service.dart';
|
||||
import 'package:quiz_app/feature/room_maker/controller/room_maker_controller.dart';
|
||||
|
||||
class RoomMakerBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => RoomMakerController());
|
||||
Get.lazyPut(() => SessionService());
|
||||
Get.lazyPut(() => RoomMakerController(Get.find<SessionService>(), Get.find<UserController>()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
import 'package:flutter/material.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/dto/waiting_room_dto.dart';
|
||||
import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
|
||||
import 'package:quiz_app/data/models/session/session_request_model.dart';
|
||||
import 'package:quiz_app/data/services/session_service.dart';
|
||||
|
||||
class RoomMakerController extends GetxController {
|
||||
final roomName = ''.obs;
|
||||
final SessionService _sessionService;
|
||||
final UserController _userController;
|
||||
|
||||
RoomMakerController(this._sessionService, this._userController);
|
||||
|
||||
// final roomName = ''.obs;
|
||||
final selectedQuiz = Rxn<QuizListingModel>();
|
||||
|
||||
RxBool isOnwQuiz = true.obs;
|
||||
|
@ -34,18 +44,24 @@ class RoomMakerController extends GetxController {
|
|||
),
|
||||
].obs;
|
||||
|
||||
void createRoom() {
|
||||
if (roomName.value.trim().isEmpty || selectedQuiz.value == null) {
|
||||
void onCreateRoom() async {
|
||||
print("room ${nameTC.text} || ${selectedQuiz.value}");
|
||||
if (nameTC.text.trim().isEmpty || selectedQuiz.value == null) {
|
||||
Get.snackbar("Gagal", "Nama room dan kuis harus dipilih.");
|
||||
return;
|
||||
}
|
||||
|
||||
final quiz = selectedQuiz.value!;
|
||||
print("Membuat room:");
|
||||
print("- Nama: ${roomName.value}");
|
||||
print("- Quiz: ${quiz.title}");
|
||||
print("- Durasi: ${quiz.duration} detik");
|
||||
print("- Jumlah Soal: ${quiz.totalQuiz}");
|
||||
|
||||
final response = await _sessionService.createSession(
|
||||
SessionRequestModel(
|
||||
quizId: quiz.quizId,
|
||||
hostId: _userController.userData!.id,
|
||||
limitParticipan: int.parse(maxPlayerTC.text),
|
||||
),
|
||||
);
|
||||
|
||||
if (response != null) Get.toNamed(AppRoutes.waitRoomPage, arguments: WaitingRoomDTO(true, response.data!));
|
||||
}
|
||||
|
||||
void onQuizSourceChange(bool base) {
|
||||
|
@ -56,6 +72,4 @@ class RoomMakerController extends GetxController {
|
|||
final selected = availableQuizzes.firstWhere((e) => e.quizId == quizId);
|
||||
selectedQuiz.value = selected;
|
||||
}
|
||||
|
||||
void onCreateRoom() {}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
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/waiting_room/controller/waiting_room_controller.dart';
|
||||
|
||||
class WaitingRoomBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.put(SocketService());
|
||||
Get.lazyPut<WaitingRoomController>(() => WaitingRoomController(
|
||||
Get.find<SocketService>(),
|
||||
Get.find<UserController>(),
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.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/models/quiz/quiz_listing_model.dart';
|
||||
import 'package:quiz_app/data/models/session/session_response_model.dart';
|
||||
import 'package:quiz_app/data/models/user/user_model.dart';
|
||||
import 'package:quiz_app/data/services/socket_service.dart';
|
||||
|
||||
class WaitingRoomController extends GetxController {
|
||||
final SocketService _socketService;
|
||||
final UserController _userController;
|
||||
WaitingRoomController(this._socketService, this._userController);
|
||||
|
||||
final sessionCode = ''.obs;
|
||||
final quizMeta = Rx<QuizListingModel?>(null);
|
||||
final joinedUsers = <UserModel>[].obs;
|
||||
|
||||
final isAdmin = true.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadDummyData();
|
||||
}
|
||||
|
||||
void _loadDummyData() {
|
||||
final data = Get.arguments as WaitingRoomDTO;
|
||||
|
||||
SessionResponseModel? roomData = data.data;
|
||||
isAdmin.value = data.isAdmin;
|
||||
|
||||
_socketService.initSocketConnection();
|
||||
_socketService.joinRoom(sessionCode: roomData.sessionCode, userId: _userController.userData!.id);
|
||||
|
||||
_socketService.roomMessages.listen((data) {
|
||||
final user = data["data"];
|
||||
joinedUsers.assign(UserModel(id: user['user_id'], name: user['username']));
|
||||
});
|
||||
sessionCode.value = roomData.sessionCode;
|
||||
|
||||
quizMeta.value = QuizListingModel(
|
||||
quizId: "q123",
|
||||
authorId: "a123",
|
||||
authorName: "Admin",
|
||||
title: "Uji Coba Kuis",
|
||||
description: "Kuis untuk testing",
|
||||
date: DateTime.now().toIso8601String(),
|
||||
totalQuiz: 5,
|
||||
duration: 900,
|
||||
);
|
||||
}
|
||||
|
||||
void copySessionCode(BuildContext context) {
|
||||
Clipboard.setData(ClipboardData(text: sessionCode.value));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Session code disalin!')),
|
||||
);
|
||||
}
|
||||
|
||||
void addUser(UserModel user) {
|
||||
joinedUsers.add(user);
|
||||
}
|
||||
|
||||
void startQuiz() {
|
||||
print("Mulai kuis dengan session: ${sessionCode.value}");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:quiz_app/component/global_button.dart';
|
||||
import 'package:quiz_app/data/models/user/user_model.dart';
|
||||
import 'package:quiz_app/feature/waiting_room/controller/waiting_room_controller.dart';
|
||||
import 'package:quiz_app/app/const/colors/app_colors.dart';
|
||||
|
||||
class WaitingRoomView extends GetView<WaitingRoomController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(title: const Text("Waiting Room")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
final session = controller.sessionCode.value;
|
||||
final quiz = controller.quizMeta.value;
|
||||
final users = controller.joinedUsers;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildQuizMeta(quiz),
|
||||
const SizedBox(height: 20),
|
||||
_buildSessionCode(context, session),
|
||||
const SizedBox(height: 20),
|
||||
const Text("Peserta yang Bergabung:", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(child: Obx(() => _buildUserList(users.toList()))),
|
||||
const SizedBox(height: 16),
|
||||
if (controller.isAdmin.value)
|
||||
GlobalButton(
|
||||
text: "Mulai Kuis",
|
||||
onPressed: controller.startQuiz,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSessionCode(BuildContext context, String code) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryBlue.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.primaryBlue),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text("Session Code: ", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
SelectableText(code, style: const TextStyle(fontSize: 16)),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
tooltip: 'Salin Kode',
|
||||
onPressed: () => controller.copySessionCode(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuizMeta(dynamic quiz) {
|
||||
if (quiz == null) return const SizedBox.shrink();
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.background,
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("Informasi Kuis:", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Text("Judul: ${quiz.title}"),
|
||||
Text("Deskripsi: ${quiz.description}"),
|
||||
Text("Jumlah Soal: ${quiz.totalQuiz}"),
|
||||
Text("Durasi: ${quiz.duration ~/ 60} menit"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserList(List<UserModel> users) {
|
||||
return ListView.separated(
|
||||
itemCount: users.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
final user = users[index];
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.background,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(child: Text(user.name[0])),
|
||||
const SizedBox(width: 12),
|
||||
Text(user.name, style: const TextStyle(fontSize: 16)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
24
pubspec.lock
24
pubspec.lock
|
@ -248,6 +248,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
lucide_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -413,6 +421,22 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
socket_io_client:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: socket_io_client
|
||||
sha256: c8471c2c6843cf308a5532ff653f2bcdb7fa9ae79d84d1179920578a06624f0d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
socket_io_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: socket_io_common
|
||||
sha256: "162fbaecbf4bf9a9372a62a341b3550b51dcef2f02f3e5830a297fd48203d45b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -42,6 +42,7 @@ dependencies:
|
|||
shared_preferences: ^2.5.3
|
||||
lucide_icons: ^0.257.0
|
||||
google_fonts: ^6.1.0
|
||||
socket_io_client: ^3.1.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in New Issue