update competition

This commit is contained in:
DimazzP 2025-02-21 11:26:48 +07:00
parent 98474010d0
commit b354d2c24b
10 changed files with 328 additions and 67 deletions

View File

@ -33,7 +33,7 @@ class SharedPrefHelper(context: Context) {
fun saveMaterialList(filterMaterial: String, materialList: List<MaterialDataModel>) { fun saveMaterialList(filterMaterial: String, materialList: List<MaterialDataModel>) {
val editor = sharedPreferences.edit() val editor = sharedPreferences.edit()
val json = Gson().toJson(materialList) val json = Gson().toJson(materialList)
editor.putString("material_list_$filterMaterial", json) editor.putString(" $filterMaterial", json)
editor.apply() editor.apply()
} }
@ -47,6 +47,28 @@ class SharedPrefHelper(context: Context) {
} }
} }
fun getCombinedMaterialList(): List<MaterialDataModel> {
val categories = listOf("animal", "limb", "family", "house")
val combinedList = mutableListOf<MaterialDataModel>()
for (category in categories) {
getMaterialList(category)?.let { combinedList.addAll(it) }
}
return combinedList
}
fun getRandomMaterials(nData: Int): List<MaterialDataModel> {
val combinedList = getCombinedMaterialList()
return if (combinedList.size <= nData) {
combinedList // Jika jumlah data kurang dari nData, kembalikan semua
} else {
combinedList.shuffled().take(nData) // Acak dan ambil sejumlah nData
}
}
fun clearMaterialList(filterMaterial: String) { fun clearMaterialList(filterMaterial: String) {
sharedPreferences.edit().remove("material_list_$filterMaterial").apply() sharedPreferences.edit().remove("material_list_$filterMaterial").apply()
} }

View File

@ -0,0 +1,9 @@
package com.example.lexilearn.data.model
sealed class QuizState {
data class QuestionLoaded(val question: MaterialDataModel, val totalScore: Int) : QuizState()
data class AnswerCorrect(val score: Int, val totalScore: Int) : QuizState()
data class AnswerWrong(val wrongAttempts: Int) : QuizState()
data class QuizFinished(val totalScore: Int) : QuizState()
data class Error(val message: String) : QuizState()
}

View File

@ -36,6 +36,44 @@ class FirebaseHelper(private val context: Context) {
}) })
} }
fun fetchAllMaterials(callback: (List<MaterialDataModel>) -> Unit) {
val databaseReference: DatabaseReference = FirebaseDatabase.getInstance().getReference("data")
databaseReference.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val allMaterials = mutableListOf<MaterialDataModel>()
for (categorySnapshot in snapshot.children) { // Loop setiap kategori (animal, limb, dll.)
val categoryKey = categorySnapshot.key ?: continue
val materialList = mutableListOf<MaterialDataModel>()
for (materialSnapshot in categorySnapshot.children) { // Loop setiap item dalam kategori
val material = materialSnapshot.getValue(MaterialDataModel::class.java)
material?.let {
allMaterials.add(it) // Tambahkan ke list utama
materialList.add(it) // Tambahkan ke list per kategori
}
}
// Simpan berdasarkan kategori (misalnya "animal", "limb", dll.)
sharedPrefHelper.saveMaterialList(categoryKey, materialList)
}
// Simpan semua data ke SharedPreferences dengan key "all"
sharedPrefHelper.saveMaterialList("all", allMaterials)
// Callback dengan seluruh data sebagai satu List<MaterialDataModel>
callback(allMaterials)
}
override fun onCancelled(error: DatabaseError) {
Log.e("FirebaseHelper", "Error fetching all materials: ${error.message}")
callback(emptyList()) // Jika terjadi error, kembalikan list kosong
}
})
}
// ✅ 🔥 Fungsi Login - Mencari user berdasarkan email dan password // ✅ 🔥 Fungsi Login - Mencari user berdasarkan email dan password
fun loginUser(email: String, password: String, callback: (Boolean, String?) -> Unit) { fun loginUser(email: String, password: String, callback: (Boolean, String?) -> Unit) {
val databaseReference: DatabaseReference = FirebaseDatabase.getInstance().getReference("users") val databaseReference: DatabaseReference = FirebaseDatabase.getInstance().getReference("users")

View File

@ -27,6 +27,22 @@ class MaterialRepository(private val context: Context) {
} }
} }
fun getAllMaterialData(filterMaterial: String, callback: (List<MaterialDataModel>) -> Unit) {
val cachedData =
sharedPrefHelper.getMaterialList(filterMaterial) // Ambil data dengan filter
if (cachedData != null) {
callback(cachedData)
} else {
firebaseHelper.fetchAllMaterials { materials ->
sharedPrefHelper.saveMaterialList(
filterMaterial,
materials
)
callback(materials)
}
}
}
fun createUser(user: UserModel, callback: (Boolean, String?) -> Unit) { fun createUser(user: UserModel, callback: (Boolean, String?) -> Unit) {
val databaseReference = FirebaseDatabase.getInstance().getReference("users") val databaseReference = FirebaseDatabase.getInstance().getReference("users")

View File

@ -13,7 +13,11 @@ class QuizRepository {
/** /**
* Fungsi ini akan membuat soal dengan mode soal & jawaban yang DITENTUKAN oleh parameter. * Fungsi ini akan membuat soal dengan mode soal & jawaban yang DITENTUKAN oleh parameter.
*/ */
fun generateQuestion(material: MaterialDataModel, questionMode: Int, answerMode: Int): QuizQuestion { fun generateQuestion(
material: MaterialDataModel,
questionMode: Int,
answerMode: Int
): QuizQuestion {
val questionType = when (questionMode) { val questionType = when (questionMode) {
1 -> listOf(QuestionType.TEXT, QuestionType.IMAGE).random() // Text atau Image 1 -> listOf(QuestionType.TEXT, QuestionType.IMAGE).random() // Text atau Image
2 -> QuestionType.AUDIO // Hanya Audio 2 -> QuestionType.AUDIO // Hanya Audio
@ -47,8 +51,23 @@ class QuizRepository {
*/ */
fun generateRandomQuestion(material: MaterialDataModel): QuizQuestion { fun generateRandomQuestion(material: MaterialDataModel): QuizQuestion {
val randomQuestionMode = Random.nextInt(1, 3) // Acak antara 1 (Text/Image) atau 2 (Audio) val randomQuestionMode = Random.nextInt(1, 3) // Acak antara 1 (Text/Image) atau 2 (Audio)
val randomAnswerMode = Random.nextInt(1, 3) // Acak antara 1 (FULL_WORD) atau 2 (SHUFFLED_LETTERS) val randomAnswerMode =
Random.nextInt(1, 3) // Acak antara 1 (FULL_WORD) atau 2 (SHUFFLED_LETTERS)
return generateQuestion(material, randomQuestionMode, randomAnswerMode) return generateQuestion(material, randomQuestionMode, randomAnswerMode)
} }
fun getRandomMaterials(
nData: Int,
materials: List<MaterialDataModel>,
callback: (List<MaterialDataModel>) -> Unit
) {
if (materials.isNotEmpty()) {
val randomMaterials = materials.shuffled().take(nData)
callback(randomMaterials)
} else {
callback(emptyList())
}
}
} }

View File

@ -3,15 +3,21 @@ package com.example.lexilearn.ui.views.pHome
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -35,6 +41,7 @@ import androidx.navigation.NavController
import com.example.lexilearn.R import com.example.lexilearn.R
import com.example.lexilearn.ui.components.AutoSizeText import com.example.lexilearn.ui.components.AutoSizeText
import com.example.lexilearn.ui.components.ButtonHome import com.example.lexilearn.ui.components.ButtonHome
import com.example.lexilearn.ui.theme.cAccent
import com.example.lexilearn.ui.theme.cGray import com.example.lexilearn.ui.theme.cGray
import com.example.lexilearn.ui.theme.cTextPrimary import com.example.lexilearn.ui.theme.cTextPrimary
import com.example.lexilearn.ui.theme.cprimary import com.example.lexilearn.ui.theme.cprimary
@ -48,6 +55,7 @@ import com.example.lexilearn.ui.theme.cwhite
fun HomeScreen(navController: NavController) { fun HomeScreen(navController: NavController) {
val viewModel: HomeViewModel = viewModel() val viewModel: HomeViewModel = viewModel()
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val leaderBoard = listOf("Tono (987)", "Budi (965)")
Column( Column(
modifier = Modifier modifier = Modifier
@ -116,26 +124,82 @@ fun HomeScreen(navController: NavController) {
) )
) )
) { ) {
Column(Modifier.padding(14.dp)) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom, modifier = Modifier
modifier = Modifier.fillMaxWidth() .fillMaxWidth()
.padding(14.dp)
) { ) {
Column {
Text( Text(
text = "Materi Terselesaikan", text = "Leaderboard hari ini :",
color = ctextWhite, color = ctextWhite,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 16.sp fontSize = 16.sp
) )
Image( Spacer(modifier = Modifier.height(10.dp))
painter = painterResource(id = R.drawable.bg_children), LazyColumn {
contentDescription = "image", itemsIndexed(leaderBoard) { index, player ->
modifier = Modifier.size(80.dp) Text(
text = "${index + 1}. $player",
color = cAccent,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
) )
} }
} }
} }
Box(
modifier = Modifier
.background(
color = cprimary,
shape = RoundedCornerShape(12.dp)
)
.width(160.dp)
.clickable {
viewModel.prepareCompetitionQuiz {dataMaterial->
navController.currentBackStackEntry
?.savedStateHandle
?.set("selectedMaterialList", dataMaterial)
navController.currentBackStackEntry
?.savedStateHandle
?.set("typeQuiz", "competition")
navController.currentBackStackEntry
?.savedStateHandle
?.set("indexValue", 0)
navController.navigate("quizScreen")
}
}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(8.dp)
) {
Text(
text = "Competition",
color = cAccent,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
Image(
painter = painterResource(id = R.drawable.trophy),
contentDescription = "image",
modifier = Modifier
.size(60.dp)
.padding(4.dp)
)
Text(
text = "Tantang dirimu dan raih peringkat tertinggi di leaderboard hari ini!",
color = ctextWhite,
textAlign = TextAlign.Center,
fontSize = 9.sp
)
}
}
}
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@ -1,6 +1,25 @@
package com.example.lexilearn.ui.views.pHome package com.example.lexilearn.ui.views.pHome
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.example.lexilearn.data.model.MaterialDataModel
import com.example.lexilearn.data.model.QuizState
import com.example.lexilearn.data.repository.MaterialRepository
import com.example.lexilearn.data.repository.QuizRepository
class HomeViewModel: ViewModel() { class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val _materialRepository = MaterialRepository(application)
private val _quizRepository = QuizRepository()
fun prepareCompetitionQuiz(callback: (List<MaterialDataModel>) -> Unit) {
_materialRepository.getAllMaterialData("all") { materials ->
_quizRepository.getRandomMaterials(10, materials) { randomData ->
randomData.forEach { println(it) }
callback(randomData)
}
}
}
} }

View File

@ -96,6 +96,7 @@ fun QuizScreen(
val minSize = 70.dp val minSize = 70.dp
val dataQuiz by quizViewModel.questionShuffled.collectAsState() val dataQuiz by quizViewModel.questionShuffled.collectAsState()
val listAnswer by quizViewModel.shuffledAnswerLetters.collectAsState() val listAnswer by quizViewModel.shuffledAnswerLetters.collectAsState()
val score by quizViewModel.totalScore.observeAsState(0)
val quizXOffset = remember { val quizXOffset = remember {
mutableStateMapOf<Int, Float>() mutableStateMapOf<Int, Float>()
@ -138,14 +139,15 @@ fun QuizScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
quizViewModel.initMaterialData(materials) quizViewModel.initMaterialData(materials)
quizViewModel.startQuiz(materials)
} }
LaunchedEffect(indexQuiz) { LaunchedEffect(indexQuiz) {
println("indexQuizValue = $indexQuiz")
if (indexQuiz < materials.size) { if (indexQuiz < materials.size) {
clearOffsets() clearOffsets()
quizViewModel.randomQuestion() quizViewModel.randomQuestion()
} else { } else {
if (typeQuiz != "competition") {
navController.getBackStackEntry("navMaterial/$typeQuiz") navController.getBackStackEntry("navMaterial/$typeQuiz")
.savedStateHandle .savedStateHandle
.set("numberUnlock", indexValue + 1) .set("numberUnlock", indexValue + 1)
@ -155,6 +157,7 @@ fun QuizScreen(
navController.popBackStack() navController.popBackStack()
} }
} }
}
LaunchedEffect(snackbarMessage) { LaunchedEffect(snackbarMessage) {
snackbarMessage?.let { message -> snackbarMessage?.let { message ->
snackbarHostState.showSnackbar(message) snackbarHostState.showSnackbar(message)
@ -171,7 +174,7 @@ fun QuizScreen(
) { ) {
GradientQuiz( GradientQuiz(
navController = navController, navController = navController,
headerText = stringResource(id = R.string.spelltitle), headerText = "${typeQuiz}-(${score})",
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
ConstraintLayout { ConstraintLayout {

View File

@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.lexilearn.data.model.MaterialDataModel import com.example.lexilearn.data.model.MaterialDataModel
import com.example.lexilearn.data.model.QuizQuestion import com.example.lexilearn.data.model.QuizQuestion
import com.example.lexilearn.data.model.QuizState
import com.example.lexilearn.data.repository.QuizRepository import com.example.lexilearn.data.repository.QuizRepository
import com.example.lexilearn.domain.models.ModelAnswerRead import com.example.lexilearn.domain.models.ModelAnswerRead
import com.example.lexilearn.domain.models.ModelSpell import com.example.lexilearn.domain.models.ModelSpell
@ -19,12 +20,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Locale import java.util.Locale
import kotlin.random.Random
class QuizViewModel(application: Application) : AndroidViewModel(application), class QuizViewModel(application: Application) : AndroidViewModel(application),
TextToSpeech.OnInitListener { TextToSpeech.OnInitListener {
private val quizRepository = QuizRepository() private val quizRepository = QuizRepository()
private var materialList: List<MaterialDataModel> = emptyList() private var materialList: List<MaterialDataModel> = emptyList()
private val _currentQuestion = MutableStateFlow<QuizQuestion?>(null) private val _currentQuestion = MutableStateFlow<QuizQuestion?>(null)
@ -78,30 +79,17 @@ class QuizViewModel(application: Application) : AndroidViewModel(application),
materialList = material; materialList = material;
} }
// fun randomQuestion() {
// _currentQuestion.value = quizRepository.generateQuestion(materialList[_indexQuiz.value], 2, 2)
// val dataQuest =
// _currentQuestion.value?.correctAnswer?.correctWord?.mapIndexed { index, char ->
// ModelSpell(index + 1, true, data = "coba $char", showCard = false)
// } ?: emptyList()
// _questionShuffled.value = dataQuest
//
// val listAnswer =
// _currentQuestion.value?.correctAnswer?.correctWord?.mapIndexed { index, char ->
// ModelAnswerRead(index + 1, char.toString())
// } ?: emptyList()
// val shuflledAnswer = listAnswer.shuffled()
// _shuffledAnswerLetters.value = shuflledAnswer
// println("testlistanswer ${_shuffledAnswerLetters.value}")
// }
// Di dalam fungsi randomQuestion() pada QuizViewModel
fun randomQuestion() {
_currentQuestion.value = quizRepository.generateQuestion(materialList[_indexQuiz.value], 2, 2)
// Generate unique IDs berdasarkan indexQuiz dan indeks karakter fun randomQuestion() {
val dataQuest = _currentQuestion.value?.correctAnswer?.correctWord?.mapIndexed { index, char -> val randomQuestionMode = Random.nextInt(1, 3)
_currentQuestion.value =
quizRepository.generateQuestion(materialList[_indexQuiz.value], randomQuestionMode, 2)
val dataQuest =
_currentQuestion.value?.correctAnswer?.correctWord?.mapIndexed { index, char ->
ModelSpell( ModelSpell(
id = (_indexQuiz.value * 100) + index + 1, // ID unik per pertanyaan id = (_indexQuiz.value * 100) + index + 1,
type = true, type = true,
data = "?", data = "?",
showCard = false showCard = false
@ -109,22 +97,17 @@ fun randomQuestion() {
} ?: emptyList() } ?: emptyList()
_questionShuffled.value = dataQuest _questionShuffled.value = dataQuest
val listAnswer = _currentQuestion.value?.correctAnswer?.correctWord?.mapIndexed { index, char -> val listAnswer =
_currentQuestion.value?.correctAnswer?.correctWord?.mapIndexed { index, char ->
ModelAnswerRead( ModelAnswerRead(
id = (_indexQuiz.value * 100) + index + 1, // ID unik per pertanyaan id = (_indexQuiz.value * 100) + index + 1, // ID unik per pertanyaan
data = char.toString() data = char.toString()
) )
} ?: emptyList() } ?: emptyList()
_shuffledAnswerLetters.value = listAnswer.shuffled() _shuffledAnswerLetters.value = listAnswer.shuffled()
}
fun submitAnswer(answer: String) {
_userAnswer.value = answer
_isAnswerCorrect.value =
_currentQuestion.value?.correctAnswer?.correctWord?.equals(answer, ignoreCase = true)
} }
private var tts: TextToSpeech? = null private var tts: TextToSpeech? = null
private val _isTTSInitialized = MutableLiveData(false) private val _isTTSInitialized = MutableLiveData(false)
val isTTSInitialized: LiveData<Boolean> = _isTTSInitialized val isTTSInitialized: LiveData<Boolean> = _isTTSInitialized
@ -171,8 +154,10 @@ fun randomQuestion() {
val checkData = checkAnswer(answerString); val checkData = checkAnswer(answerString);
if (checkData) { if (checkData) {
triggerSnackbar("Jawaban Benar") triggerSnackbar("Jawaban Benar")
submitAnswer(true)
incrementIndexQuiz() incrementIndexQuiz()
} else { } else {
submitAnswer(false)
triggerSnackbar("Jawaban Salah") triggerSnackbar("Jawaban Salah")
} }
_isButtonVisible.value = true _isButtonVisible.value = true
@ -197,4 +182,90 @@ fun randomQuestion() {
tts?.shutdown() tts?.shutdown()
super.onCleared() super.onCleared()
} }
// competition ====================================================================
private val _quizState = MutableLiveData<QuizState>()
val quizState: LiveData<QuizState> get() = _quizState
private val _currentScore = MutableLiveData(0) // Skor untuk soal saat ini
val currentScore: LiveData<Int> get() = _currentScore
private val _totalScore = MutableLiveData(0) // Total skor sepanjang sesi
val totalScore: LiveData<Int> get() = _totalScore
private var questionList = listOf<MaterialDataModel>()
private var currentQuestionIndex = 0
private var wrongAttempts = 0
private var startTime: Long = 0
// Konstanta untuk scoring
private val baseScore = 100
private val safeTime = 5
private val timePenalty = 3
private val wrongPenalty = 5
private val minScore = 50
private val bonusQuick = 10
/** 1⃣ Memulai Kuis **/
fun startQuiz(materials: List<MaterialDataModel>) {
if (materials.isNotEmpty()) {
questionList = materials
currentQuestionIndex = 0
_totalScore.value = 0 // Reset skor saat mulai
_currentScore.value = 0
wrongAttempts = 0
startTime = System.currentTimeMillis()
_quizState.value =
QuizState.QuestionLoaded(questionList[currentQuestionIndex], _totalScore.value ?: 0)
} else {
_quizState.value = QuizState.Error("Tidak ada soal tersedia")
}
}
/** 2⃣ Dipanggil saat jawaban dikirim **/
fun submitAnswer(isCorrect: Boolean) {
val responseTime =
((System.currentTimeMillis() - startTime) / 1000).toInt() // Hitung waktu otomatis
if (isCorrect) {
val score = calculateScore(responseTime, wrongAttempts)
_currentScore.value = score
_totalScore.value = (_totalScore.value ?: 0) + score // Update total skor
moveToNextQuestion()
} else {
wrongAnswer()
}
}
/** 3⃣ Dipanggil saat jawaban salah **/
fun wrongAnswer() {
wrongAttempts += 1
_quizState.value = QuizState.AnswerWrong(wrongAttempts)
}
/** 4⃣ Berganti ke soal berikutnya **/
private fun moveToNextQuestion() {
if (currentQuestionIndex < questionList.size - 1) {
currentQuestionIndex++
wrongAttempts = 0
startTime = System.currentTimeMillis()
_quizState.value =
QuizState.QuestionLoaded(questionList[currentQuestionIndex], _totalScore.value ?: 0)
} else {
_quizState.value = QuizState.QuizFinished(_totalScore.value ?: 0)
}
}
/** 🔢 Perhitungan Skor **/
private fun calculateScore(responseTime: Int, wrongAttempts: Int): Int {
val penaltyTime = maxOf(0, responseTime - safeTime) * timePenalty
val penaltyWrong = wrongAttempts * wrongPenalty
var score = baseScore - penaltyTime - penaltyWrong
if (responseTime <= 3) {
score += bonusQuick
}
return maxOf(minScore, score)
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB