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 0000000..26cd82d Binary files /dev/null and b/app/src/main/res/drawable/trophy.png differ