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>) {
val editor = sharedPreferences.edit()
val json = Gson().toJson(materialList)
editor.putString("material_list_$filterMaterial", json)
editor.putString(" $filterMaterial", json)
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) {
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
fun loginUser(email: String, password: String, callback: (Boolean, String?) -> Unit) {
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) {
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.
*/
fun generateQuestion(material: MaterialDataModel, questionMode: Int, answerMode: Int): QuizQuestion {
fun generateQuestion(
material: MaterialDataModel,
questionMode: Int,
answerMode: Int
): QuizQuestion {
val questionType = when (questionMode) {
1 -> listOf(QuestionType.TEXT, QuestionType.IMAGE).random() // Text atau Image
2 -> QuestionType.AUDIO // Hanya Audio
@ -47,8 +51,23 @@ class QuizRepository {
*/
fun generateRandomQuestion(material: MaterialDataModel): QuizQuestion {
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)
}
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.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
@ -35,6 +41,7 @@ import androidx.navigation.NavController
import com.example.lexilearn.R
import com.example.lexilearn.ui.components.AutoSizeText
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.cTextPrimary
import com.example.lexilearn.ui.theme.cprimary
@ -48,6 +55,7 @@ import com.example.lexilearn.ui.theme.cwhite
fun HomeScreen(navController: NavController) {
val viewModel: HomeViewModel = viewModel()
val scrollState = rememberScrollState()
val leaderBoard = listOf("Tono (987)", "Budi (965)")
Column(
modifier = Modifier
@ -116,24 +124,80 @@ fun HomeScreen(navController: NavController) {
)
)
) {
Column(Modifier.padding(14.dp)) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom,
modifier = Modifier.fillMaxWidth()
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.padding(14.dp)
) {
Column {
Text(
text = "Materi Terselesaikan",
text = "Leaderboard hari ini :",
color = ctextWhite,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
Image(
painter = painterResource(id = R.drawable.bg_children),
contentDescription = "image",
modifier = Modifier.size(80.dp)
)
Spacer(modifier = Modifier.height(10.dp))
LazyColumn {
itemsIndexed(leaderBoard) { index, player ->
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(

View File

@ -1,6 +1,25 @@
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 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 dataQuiz by quizViewModel.questionShuffled.collectAsState()
val listAnswer by quizViewModel.shuffledAnswerLetters.collectAsState()
val score by quizViewModel.totalScore.observeAsState(0)
val quizXOffset = remember {
mutableStateMapOf<Int, Float>()
@ -138,21 +139,23 @@ fun QuizScreen(
LaunchedEffect(Unit) {
quizViewModel.initMaterialData(materials)
quizViewModel.startQuiz(materials)
}
LaunchedEffect(indexQuiz) {
println("indexQuizValue = $indexQuiz")
if (indexQuiz < materials.size) {
clearOffsets()
quizViewModel.randomQuestion()
} else {
navController.getBackStackEntry("navMaterial/$typeQuiz")
.savedStateHandle
.set("numberUnlock", indexValue + 1)
println("printUpdateUnlock-quiz $indexValue")
// Kemudian kembali ke halaman sebelumnya
navController.popBackStack()
navController.popBackStack()
if (typeQuiz != "competition") {
navController.getBackStackEntry("navMaterial/$typeQuiz")
.savedStateHandle
.set("numberUnlock", indexValue + 1)
println("printUpdateUnlock-quiz $indexValue")
// Kemudian kembali ke halaman sebelumnya
navController.popBackStack()
navController.popBackStack()
}
}
}
LaunchedEffect(snackbarMessage) {
@ -171,7 +174,7 @@ fun QuizScreen(
) {
GradientQuiz(
navController = navController,
headerText = stringResource(id = R.string.spelltitle),
headerText = "${typeQuiz}-(${score})",
modifier = Modifier.fillMaxSize()
) {
ConstraintLayout {

View File

@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.example.lexilearn.data.model.MaterialDataModel
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.domain.models.ModelAnswerRead
import com.example.lexilearn.domain.models.ModelSpell
@ -19,12 +20,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.util.Locale
import kotlin.random.Random
class QuizViewModel(application: Application) : AndroidViewModel(application),
TextToSpeech.OnInitListener {
private val quizRepository = QuizRepository()
private var materialList: List<MaterialDataModel> = emptyList()
private val _currentQuestion = MutableStateFlow<QuizQuestion?>(null)
@ -78,53 +79,35 @@ class QuizViewModel(application: Application) : AndroidViewModel(application),
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
val dataQuest = _currentQuestion.value?.correctAnswer?.correctWord?.mapIndexed { index, char ->
ModelSpell(
id = (_indexQuiz.value * 100) + index + 1, // ID unik per pertanyaan
type = true,
data = "?",
showCard = false
)
} ?: emptyList()
_questionShuffled.value = dataQuest
fun randomQuestion() {
val randomQuestionMode = Random.nextInt(1, 3)
val listAnswer = _currentQuestion.value?.correctAnswer?.correctWord?.mapIndexed { index, char ->
ModelAnswerRead(
id = (_indexQuiz.value * 100) + index + 1, // ID unik per pertanyaan
data = char.toString()
)
} ?: emptyList()
_shuffledAnswerLetters.value = listAnswer.shuffled()
}
_currentQuestion.value =
quizRepository.generateQuestion(materialList[_indexQuiz.value], randomQuestionMode, 2)
val dataQuest =
_currentQuestion.value?.correctAnswer?.correctWord?.mapIndexed { index, char ->
ModelSpell(
id = (_indexQuiz.value * 100) + index + 1,
type = true,
data = "?",
showCard = false
)
} ?: emptyList()
_questionShuffled.value = dataQuest
fun submitAnswer(answer: String) {
_userAnswer.value = answer
_isAnswerCorrect.value =
_currentQuestion.value?.correctAnswer?.correctWord?.equals(answer, ignoreCase = true)
val listAnswer =
_currentQuestion.value?.correctAnswer?.correctWord?.mapIndexed { index, char ->
ModelAnswerRead(
id = (_indexQuiz.value * 100) + index + 1, // ID unik per pertanyaan
data = char.toString()
)
} ?: emptyList()
_shuffledAnswerLetters.value = listAnswer.shuffled()
}
private var tts: TextToSpeech? = null
private val _isTTSInitialized = MutableLiveData(false)
val isTTSInitialized: LiveData<Boolean> = _isTTSInitialized
@ -171,8 +154,10 @@ fun randomQuestion() {
val checkData = checkAnswer(answerString);
if (checkData) {
triggerSnackbar("Jawaban Benar")
submitAnswer(true)
incrementIndexQuiz()
} else {
submitAnswer(false)
triggerSnackbar("Jawaban Salah")
}
_isButtonVisible.value = true
@ -197,4 +182,90 @@ fun randomQuestion() {
tts?.shutdown()
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