From b354d2c24bf83119eeeb38dd36b1508c862658c3 Mon Sep 17 00:00:00 2001 From: DimazzP Date: Fri, 21 Feb 2025 11:26:48 +0700 Subject: [PATCH] update competition --- .../lexilearn/data/local/SharedPrefHelper.kt | 24 ++- .../example/lexilearn/data/model/QuizState.kt | 9 + .../lexilearn/data/remote/FirebaseHelper.kt | 38 +++++ .../data/repository/MaterialRepository.kt | 16 ++ .../data/repository/QuizRepository.kt | 23 ++- .../lexilearn/ui/views/pHome/HomeScreen.kt | 88 ++++++++-- .../lexilearn/ui/views/pHome/HomeViewModel.kt | 21 ++- .../lexilearn/ui/views/pQuiz/QuizScreen.kt | 21 ++- .../lexilearn/ui/views/pQuiz/QuizViewModel.kt | 155 +++++++++++++----- app/src/main/res/drawable/trophy.png | Bin 0 -> 5023 bytes 10 files changed, 328 insertions(+), 67 deletions(-) create mode 100644 app/src/main/java/com/example/lexilearn/data/model/QuizState.kt create mode 100644 app/src/main/res/drawable/trophy.png diff --git a/app/src/main/java/com/example/lexilearn/data/local/SharedPrefHelper.kt b/app/src/main/java/com/example/lexilearn/data/local/SharedPrefHelper.kt index 48c64c8..cb60d83 100644 --- a/app/src/main/java/com/example/lexilearn/data/local/SharedPrefHelper.kt +++ b/app/src/main/java/com/example/lexilearn/data/local/SharedPrefHelper.kt @@ -33,7 +33,7 @@ class SharedPrefHelper(context: Context) { fun saveMaterialList(filterMaterial: String, materialList: List) { 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 { + val categories = listOf("animal", "limb", "family", "house") + val combinedList = mutableListOf() + + for (category in categories) { + getMaterialList(category)?.let { combinedList.addAll(it) } + } + + return combinedList + } + + fun getRandomMaterials(nData: Int): List { + 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() } diff --git a/app/src/main/java/com/example/lexilearn/data/model/QuizState.kt b/app/src/main/java/com/example/lexilearn/data/model/QuizState.kt new file mode 100644 index 0000000..a8dc5f9 --- /dev/null +++ b/app/src/main/java/com/example/lexilearn/data/model/QuizState.kt @@ -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() +} diff --git a/app/src/main/java/com/example/lexilearn/data/remote/FirebaseHelper.kt b/app/src/main/java/com/example/lexilearn/data/remote/FirebaseHelper.kt index d736b9f..8f6f614 100644 --- a/app/src/main/java/com/example/lexilearn/data/remote/FirebaseHelper.kt +++ b/app/src/main/java/com/example/lexilearn/data/remote/FirebaseHelper.kt @@ -36,6 +36,44 @@ class FirebaseHelper(private val context: Context) { }) } + fun fetchAllMaterials(callback: (List) -> Unit) { + val databaseReference: DatabaseReference = FirebaseDatabase.getInstance().getReference("data") + + databaseReference.addListenerForSingleValueEvent(object : ValueEventListener { + override fun onDataChange(snapshot: DataSnapshot) { + val allMaterials = mutableListOf() + + for (categorySnapshot in snapshot.children) { // Loop setiap kategori (animal, limb, dll.) + val categoryKey = categorySnapshot.key ?: continue + val materialList = mutableListOf() + + 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 + 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") diff --git a/app/src/main/java/com/example/lexilearn/data/repository/MaterialRepository.kt b/app/src/main/java/com/example/lexilearn/data/repository/MaterialRepository.kt index bc89911..351e4d8 100644 --- a/app/src/main/java/com/example/lexilearn/data/repository/MaterialRepository.kt +++ b/app/src/main/java/com/example/lexilearn/data/repository/MaterialRepository.kt @@ -27,6 +27,22 @@ class MaterialRepository(private val context: Context) { } } + fun getAllMaterialData(filterMaterial: String, callback: (List) -> 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") diff --git a/app/src/main/java/com/example/lexilearn/data/repository/QuizRepository.kt b/app/src/main/java/com/example/lexilearn/data/repository/QuizRepository.kt index b45f520..d09a77f 100644 --- a/app/src/main/java/com/example/lexilearn/data/repository/QuizRepository.kt +++ b/app/src/main/java/com/example/lexilearn/data/repository/QuizRepository.kt @@ -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, + callback: (List) -> Unit + ) { + if (materials.isNotEmpty()) { + val randomMaterials = materials.shuffled().take(nData) + callback(randomMaterials) + } else { + callback(emptyList()) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/lexilearn/ui/views/pHome/HomeScreen.kt b/app/src/main/java/com/example/lexilearn/ui/views/pHome/HomeScreen.kt index d25befb..c1c2301 100644 --- a/app/src/main/java/com/example/lexilearn/ui/views/pHome/HomeScreen.kt +++ b/app/src/main/java/com/example/lexilearn/ui/views/pHome/HomeScreen.kt @@ -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( diff --git a/app/src/main/java/com/example/lexilearn/ui/views/pHome/HomeViewModel.kt b/app/src/main/java/com/example/lexilearn/ui/views/pHome/HomeViewModel.kt index 8995b6d..b902430 100644 --- a/app/src/main/java/com/example/lexilearn/ui/views/pHome/HomeViewModel.kt +++ b/app/src/main/java/com/example/lexilearn/ui/views/pHome/HomeViewModel.kt @@ -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) -> Unit) { + _materialRepository.getAllMaterialData("all") { materials -> + _quizRepository.getRandomMaterials(10, materials) { randomData -> + randomData.forEach { println(it) } + callback(randomData) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizScreen.kt b/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizScreen.kt index 21fdb8a..15e1acd 100644 --- a/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizScreen.kt +++ b/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizScreen.kt @@ -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() @@ -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 { diff --git a/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizViewModel.kt b/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizViewModel.kt index 9f294fc..7f510b5 100644 --- a/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizViewModel.kt +++ b/app/src/main/java/com/example/lexilearn/ui/views/pQuiz/QuizViewModel.kt @@ -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 = emptyList() private val _currentQuestion = MutableStateFlow(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 = _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() + val quizState: LiveData get() = _quizState + + private val _currentScore = MutableLiveData(0) // Skor untuk soal saat ini + val currentScore: LiveData get() = _currentScore + + private val _totalScore = MutableLiveData(0) // Total skor sepanjang sesi + val totalScore: LiveData get() = _totalScore + + private var questionList = listOf() + 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) { + 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) + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/trophy.png b/app/src/main/res/drawable/trophy.png new file mode 100644 index 0000000000000000000000000000000000000000..26cd82de64c00e395d7e84117e865519b338f79a GIT binary patch literal 5023 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