update live score

This commit is contained in:
DimazzP 2025-02-26 00:23:04 +07:00
parent b500df7296
commit 217f587965
8 changed files with 268 additions and 59 deletions

View File

@ -72,7 +72,7 @@ dependencies {
implementation("io.coil-kt:coil-compose:2.1.0") implementation("io.coil-kt:coil-compose:2.1.0")
implementation ("com.google.code.gson:gson:2.10.1") implementation ("com.google.code.gson:gson:2.10.1")
implementation("com.google.accompanist:accompanist-flowlayout:0.30.1") implementation("com.google.accompanist:accompanist-flowlayout:0.30.1")
implementation("com.airbnb.android:lottie-compose:6.3.0")
implementation(platform("com.google.firebase:firebase-bom:32.0.0")) implementation(platform("com.google.firebase:firebase-bom:32.0.0"))
implementation("com.google.firebase:firebase-analytics") implementation("com.google.firebase:firebase-analytics")

View File

@ -0,0 +1 @@
{"v":"5.5.8","fr":60,"ip":0,"op":181,"w":200,"h":200,"nm":"Locked","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":4,"ty":3,"nm":"Null 1","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[1],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.664],"y":[0.825]},"o":{"x":[1],"y":[0]},"t":3,"s":[-10.402]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.341],"y":[0.229]},"t":6,"s":[0]},{"i":{"x":[0.695],"y":[0.873]},"o":{"x":[1],"y":[0]},"t":9,"s":[8]},{"i":{"x":[0.253],"y":[1]},"o":{"x":[1],"y":[0.779]},"t":12,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[1],"y":[0]},"t":15,"s":[-4]},{"i":{"x":[0.65],"y":[1.342]},"o":{"x":[0.167],"y":[0]},"t":18,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.341],"y":[-0.229]},"t":45,"s":[0]},{"i":{"x":[0.664],"y":[0.773]},"o":{"x":[1],"y":[0]},"t":47,"s":[-8]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.341],"y":[0.229]},"t":49,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":51,"s":[8]},{"t":53,"s":[0]}],"ix":10},"p":{"a":0,"k":[99,149,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[34,34,100],"ix":6}},"ao":0,"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"shackle","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":48,"s":[3.432,-174.816,0],"to":[0,-5.451,0],"ti":[0,5.451,0]},{"t":69,"s":[3.432,-207.522,0]}],"ix":2},"a":{"a":0,"k":[100.167,89.562,0],"ix":1},"s":{"a":0,"k":[294.118,294.118,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":48,"s":[{"i":[[0,0],[0,0],[-14.045,0],[0,-14.045],[0,0]],"o":[[0,0],[0,-14.045],[14.044,0],[0,0],[0,0]],"v":[[-25.43,42.438],[-25.43,-17.008],[0,-42.438],[25.43,-17.008],[25.36,-0.563]],"c":false}]},{"t":59,"s":[{"i":[[0,0],[0,0],[-14.045,0],[0,-14.045],[0,0]],"o":[[0,0],[0,-14.045],[14.044,0],[0,0],[0,0]],"v":[[-25.43,42.438],[-25.43,-17.008],[0,-42.438],[25.43,-17.008],[25.47,-11.063]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.10980392156862745,0.10980392156862745,0.10980392156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[100.167,89.562],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"body","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2.942,-93.479,0],"ix":2},"a":{"a":0,"k":[100,117.217,0],"ix":1},"s":{"a":0,"k":[294.118,294.118,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,-4.226],[0,0],[-3.123,0],[0,0],[0,4.204],[0,0],[3.123,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3.123,0],[0,0],[0,4.204],[0,0],[3.123,0],[0,0],[0.021,-4.226],[0,0],[0,0]],"v":[[18.941,-32.783],[0.951,-32.783],[-0.953,-32.783],[-18.942,-32.783],[-32.31,-32.783],[-34.343,-32.783],[-40.01,-25.167],[-40.01,25.144],[-34.343,32.783],[34.32,32.783],[39.989,25.144],[39.989,-25.167],[34.341,-32.783],[32.31,-32.783]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[1.605,-1.672],[0,0],[1.668,0],[0,0],[0,1.74],[0,0],[0,2.554],[-4.491,0.181],[-0.47,-0.023],[0,-4.814]],"o":[[0,0],[0,1.74],[0,0],[-1.647,0],[0,0],[-1.604,-1.672],[0,-4.814],[0.471,-0.023],[4.492,0.181],[0,2.532]],"v":[[6.428,-0.057],[6.428,15.154],[3.391,18.363],[-3.412,18.363],[-6.45,15.154],[-6.45,-0.057],[-8.974,-6.566],[-0.953,-15.719],[0.951,-15.719],[8.973,-6.566]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.10980392156862745,0.10980392156862745,0.10980392156862745,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[100.011,117.217],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":900,"st":0,"bm":0}],"markers":[]}

View File

@ -0,0 +1,38 @@
import android.util.Log
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.*
@Composable
fun LottieAnimationComponent(
modifier: Modifier = Modifier,
assetName: String,
playOnce: Boolean = true
) {
val composition by rememberLottieComposition(LottieCompositionSpec.Asset(assetName))
var isPlaying by remember { mutableStateOf(false) }
// Cek apakah composition berhasil dimuat sebelum menampilkan animasi
if (composition != null) {
val progress by animateLottieCompositionAsState(
composition = composition,
isPlaying = isPlaying,
restartOnPlay = !playOnce
)
LottieAnimation(
composition = composition,
progress = { progress },
modifier = modifier
)
// Jika playOnce = false, jalankan animasi otomatis
LaunchedEffect(Unit) {
isPlaying = true
}
} else {
Log.e("LottieAnimation", "Failed to load Lottie animation: $assetName")
}
}

View File

@ -1,5 +1,6 @@
package com.example.lexilearn.ui.views.pNavMaterial package com.example.lexilearn.ui.views.pNavMaterial
import LottieAnimationComponent
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.clickable
@ -58,6 +59,7 @@ fun NavMaterialScreen(navController: NavController, materialId: String) {
} }
val materialList by viewModel.materialList.collectAsState() val materialList by viewModel.materialList.collectAsState()
val navLockPos by viewModel.navLockPosition.collectAsState()
val textTitle by viewModel.textTitle.observeAsState("") val textTitle by viewModel.textTitle.observeAsState("")
val sizeUnlock by viewModel.sizeUnlock.observeAsState() val sizeUnlock by viewModel.sizeUnlock.observeAsState()
@ -82,7 +84,7 @@ fun NavMaterialScreen(navController: NavController, materialId: String) {
) { ) {
itemsIndexed(materialList) { index, chunk -> itemsIndexed(materialList) { index, chunk ->
ConstraintLayout(modifier = Modifier.fillMaxWidth()) { ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
val (space, boxItem, centerLine, lockIcon) = createRefs() val (space, boxItem, centerLine, lockIcon, lockLottie) = createRefs()
Box(modifier = Modifier Box(modifier = Modifier
.background(color = cwhite, shape = RoundedCornerShape(12.dp)) .background(color = cwhite, shape = RoundedCornerShape(12.dp))
.blur(if ((sizeUnlock ?: 0) < index) 16.dp else 0.dp) .blur(if ((sizeUnlock ?: 0) < index) 16.dp else 0.dp)
@ -209,6 +211,20 @@ fun NavMaterialScreen(navController: NavController, materialId: String) {
end.linkTo(parent.end) end.linkTo(parent.end)
} }
) )
} else {
if (navLockPos == index) {
LottieAnimationComponent(assetName = "unlock_lotties.json",
playOnce = true,
modifier = Modifier
.size(60.dp)
.constrainAs(lockLottie) {
start.linkTo(parent.start)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
})
}
} }
} }
} }

View File

@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope
import com.example.lexilearn.data.model.MaterialDataModel import com.example.lexilearn.data.model.MaterialDataModel
import com.example.lexilearn.data.repository.MaterialRepository import com.example.lexilearn.data.repository.MaterialRepository
import com.example.lexilearn.utils.generateNumberList import com.example.lexilearn.utils.generateNumberList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -28,6 +29,9 @@ class NavMaterialViewModel(application: Application) : AndroidViewModel(applicat
private val _sizeUnlock = MutableLiveData(0) private val _sizeUnlock = MutableLiveData(0)
val sizeUnlock: LiveData<Int> = _sizeUnlock val sizeUnlock: LiveData<Int> = _sizeUnlock
private val _navLockPosition = MutableStateFlow(-1)
val navLockPosition: StateFlow<Int> = _navLockPosition
private val repository = MaterialRepository(application) private val repository = MaterialRepository(application)
private val _materialList = MutableStateFlow<List<List<MaterialDataModel>>>(emptyList()) private val _materialList = MutableStateFlow<List<List<MaterialDataModel>>>(emptyList())
@ -80,6 +84,9 @@ class NavMaterialViewModel(application: Application) : AndroidViewModel(applicat
println("numberUnlock update unlock value success $it") println("numberUnlock update unlock value success $it")
fetchMaterial(materialId) fetchMaterial(materialId)
} }
_navLockPosition.value = value
delay(1800)
_navLockPosition.value = -1
} }
}else{ }else{
println("numberUnlock-else $value, ${_sizeUnlock.value}, ${_materialList.value.size}") println("numberUnlock-else $value, ${_sizeUnlock.value}, ${_materialList.value.size}")

View File

@ -36,8 +36,8 @@ import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
@ -47,19 +47,18 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import com.example.lexilearn.R
import com.example.lexilearn.data.model.QuestionType import com.example.lexilearn.data.model.QuestionType
import com.example.lexilearn.ui.components.CardQuiz import com.example.lexilearn.ui.components.CardQuiz
import com.example.lexilearn.ui.components.DraggableAnswerCard import com.example.lexilearn.ui.components.DraggableAnswerCard
import com.example.lexilearn.ui.components.FirebaseImage import com.example.lexilearn.ui.components.FirebaseImage
import com.example.lexilearn.ui.components.GradientQuiz import com.example.lexilearn.ui.components.GradientQuiz
import com.example.lexilearn.ui.components.MyShadowCard import com.example.lexilearn.ui.components.MyShadowCard
import com.example.lexilearn.ui.theme.cAccent
import com.example.lexilearn.ui.theme.ctextGray import com.example.lexilearn.ui.theme.ctextGray
import com.example.lexilearn.ui.theme.ctextWhite import com.example.lexilearn.ui.theme.ctextWhite
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
@ -91,12 +90,18 @@ fun QuizScreen(
mutableStateMapOf<Int, Dp>() mutableStateMapOf<Int, Dp>()
} }
val maxSize = 120.dp val maxSize = 90.dp
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 totalScore by quizViewModel.totalScore.collectAsState()
val liveCurrentScore by quizViewModel.liveCurrentScore.collectAsState()
val formattedTime by quizViewModel.formattedResponseTime.collectAsState()
val scoreReduction by quizViewModel.scoreReduction.collectAsState()
val responseTimeMs by quizViewModel.responseTimeMs.collectAsState()
val quizXOffset = remember { val quizXOffset = remember {
mutableStateMapOf<Int, Float>() mutableStateMapOf<Int, Float>()
@ -174,7 +179,7 @@ fun QuizScreen(
) { ) {
GradientQuiz( GradientQuiz(
navController = navController, navController = navController,
headerText = "${typeQuiz}-(${score})", headerText = "${typeQuiz}",
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
ConstraintLayout { ConstraintLayout {
@ -195,6 +200,33 @@ fun QuizScreen(
color = ctextWhite color = ctextWhite
) )
} }
if (typeQuiz == "competition") {
Text(
text = "Total Score: $totalScore",
color = ctextWhite,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Row(
Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Score: $liveCurrentScore",
color = cAccent,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Text(
text = "Time: ${formattedTime}",
color = cAccent,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
}
}
MyShadowCard( MyShadowCard(
modifier = Modifier modifier = Modifier
.padding(12.dp) .padding(12.dp)
@ -220,17 +252,26 @@ fun QuizScreen(
modifier = Modifier.padding(12.dp) modifier = Modifier.padding(12.dp)
) )
} else if (question?.questionType == QuestionType.AUDIO) { } else if (question?.questionType == QuestionType.AUDIO) {
IconButton( Button(
onClick = { onClick = {
quizViewModel.speakLetter(question!!.question) quizViewModel.speakLetter(question!!.question)
}, },
) { ) {
Icon( Column(
imageVector = Icons.Filled.VolumeUp, horizontalAlignment = Alignment.CenterHorizontally
contentDescription = "Speaker Icon", ) {
tint = Color.Black, Icon(
modifier = Modifier.size(200.dp) imageVector = Icons.Filled.VolumeUp,
) contentDescription = "Speaker Icon",
tint = Color.White,
modifier = Modifier.size(50.dp) // Ukuran ikon lebih kecil dari 200dp agar lebih seimbang
)
Text(
text = "Click",
fontSize = 14.sp,
color = Color.White
)
}
} }
} }
} }
@ -402,7 +443,7 @@ fun QuizScreen(
rectColumnAnswer = it.boundsInWindow() rectColumnAnswer = it.boundsInWindow()
}, },
) { ) {
listAnswer.chunked(3).forEach { rowItems -> listAnswer.chunked(4).forEach { rowItems ->
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center

View File

@ -18,8 +18,10 @@ import com.example.lexilearn.domain.models.ModelSpell
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Locale import java.util.Locale
import kotlin.math.max
import kotlin.random.Random import kotlin.random.Random
class QuizViewModel(application: Application) : AndroidViewModel(application), class QuizViewModel(application: Application) : AndroidViewModel(application),
@ -120,6 +122,7 @@ class QuizViewModel(application: Application) : AndroidViewModel(application),
override fun onInit(status: Int) { override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) { if (status == TextToSpeech.SUCCESS) {
tts?.language = Locale.ENGLISH tts?.language = Locale.ENGLISH
tts?.setSpeechRate(0.5f)
_isTTSInitialized.value = true _isTTSInitialized.value = true
} else { } else {
_isTTSInitialized.value = false _isTTSInitialized.value = false
@ -133,7 +136,7 @@ class QuizViewModel(application: Application) : AndroidViewModel(application),
} }
fun checkAnswer(answerString: String): Boolean { fun checkAnswer(answerString: String): Boolean {
if (answerString == currentQuestion.value?.correctAnswer?.correctWord) { if (answerString.lowercase() == currentQuestion.value?.correctAnswer?.correctWord?.lowercase()) {
return true return true
} }
return false return false
@ -184,81 +187,187 @@ class QuizViewModel(application: Application) : AndroidViewModel(application),
} }
// competition ==================================================================== // competition ====================================================================
private val _quizState = MutableLiveData<QuizState>() // private val _quizState = MutableLiveData<QuizState>()
val quizState: LiveData<QuizState> get() = _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)
// }
private val _totalScore = MutableStateFlow(0)
val totalScore: StateFlow<Int> get() = _totalScore.asStateFlow()
private val _currentScore = MutableLiveData(0) // Skor untuk soal saat ini private val _scoreReduction = MutableStateFlow(100) // Nilai awal (100)
val currentScore: LiveData<Int> get() = _currentScore val scoreReduction: StateFlow<Int> get() = _scoreReduction.asStateFlow()
private val _totalScore = MutableLiveData(0) // Total skor sepanjang sesi private val _responseTimeMs = MutableStateFlow(0L) // Stopwatch waktu jawaban
val totalScore: LiveData<Int> get() = _totalScore val responseTimeMs: StateFlow<Long> get() = _responseTimeMs.asStateFlow()
private val _liveCurrentScore = MutableStateFlow(100) // Skor live
val liveCurrentScore: StateFlow<Int> get() = _liveCurrentScore.asStateFlow()
private val _formattedResponseTime = MutableStateFlow("0.0")
val formattedResponseTime: StateFlow<String> get() = _formattedResponseTime.asStateFlow()
private var questionList = listOf<MaterialDataModel>() private var questionList = listOf<MaterialDataModel>()
private var currentQuestionIndex = 0 private var currentQuestionIndex = 0
private var wrongAttempts = 0 private var wrongAttempts = 0
private var startTime: Long = 0 private var startTime: Long = 0
private var isRunning = false
// Konstanta untuk scoring // Konstanta skor
private val baseScore = 100 private val baseScore = 100
private val safeTime = 5 private val safeTime = 5 // Waktu aman tanpa penalti (detik)
private val timePenalty = 3 private val timePenalty = 1 // Penalti per detik setelah safeTime
private val wrongPenalty = 5 private val wrongPenalty = 5 // Penalti per jawaban salah
private val minScore = 50 private val minScore = 50 // Skor minimum per soal
private val bonusQuick = 10 private val bonusQuick = 10 // Bonus jika menjawab dalam ≤ 3 detik
/** 1⃣ Memulai Kuis **/ /** Memulai Kuis **/
fun startQuiz(materials: List<MaterialDataModel>) { fun startQuiz(materials: List<MaterialDataModel>) {
if (materials.isNotEmpty()) { if (materials.isNotEmpty()) {
questionList = materials questionList = materials
currentQuestionIndex = 0 currentQuestionIndex = 0
_totalScore.value = 0 // Reset skor saat mulai _totalScore.value = 0
_currentScore.value = 0 _scoreReduction.value = baseScore // Reset ke 100
_responseTimeMs.value = 0
_liveCurrentScore.value = baseScore
wrongAttempts = 0 wrongAttempts = 0
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
_quizState.value = startStopwatch()
QuizState.QuestionLoaded(questionList[currentQuestionIndex], _totalScore.value ?: 0)
} else {
_quizState.value = QuizState.Error("Tidak ada soal tersedia")
} }
} }
/** 2⃣ Dipanggil saat jawaban dikirim **/ /** Memulai Stopwatch & Update Skor Live **/
fun submitAnswer(isCorrect: Boolean) { private fun startStopwatch() {
val responseTime = isRunning = true
((System.currentTimeMillis() - startTime) / 1000).toInt() // Hitung waktu otomatis viewModelScope.launch {
while (isRunning) {
val elapsedTime = System.currentTimeMillis() - startTime
_responseTimeMs.value = elapsedTime // Stopwatch berjalan
_liveCurrentScore.value = calculateRealTimeScore((elapsedTime / 1000).toInt(), wrongAttempts)
_formattedResponseTime.value = String.format("%.1f", elapsedTime / 1000.0)
delay(100) // Update setiap 100ms
}
}
}
/** Saat jawaban dikirim **/
fun submitAnswer(isCorrect: Boolean) {
if (isCorrect) { if (isCorrect) {
val score = calculateScore(responseTime, wrongAttempts) isRunning = false // Hentikan stopwatch
_currentScore.value = score val responseTimeSec = (_responseTimeMs.value / 1000).toInt()
_totalScore.value = (_totalScore.value ?: 0) + score // Update total skor val finalScore = calculateRealTimeScore(responseTimeSec, wrongAttempts) // Skor final
moveToNextQuestion()
_totalScore.value += finalScore // Tambahkan ke total skor
viewModelScope.launch {
delay(1000) // Tampilkan skor selama 1 detik sebelum pindah ke soal berikutnya
moveToNextQuestion()
}
} else { } else {
wrongAnswer() wrongAnswer()
} }
} }
/** 3⃣ Dipanggil saat jawaban salah **/ /** Saat jawaban salah **/
fun wrongAnswer() { fun wrongAnswer() {
wrongAttempts += 1 wrongAttempts += 1
_quizState.value = QuizState.AnswerWrong(wrongAttempts) _liveCurrentScore.value = calculateRealTimeScore((_responseTimeMs.value / 1000).toInt(), wrongAttempts)
} }
/** 4⃣ Berganti ke soal berikutnya **/ /** Berganti ke soal berikutnya **/
private fun moveToNextQuestion() { private fun moveToNextQuestion() {
if (currentQuestionIndex < questionList.size - 1) { if (currentQuestionIndex < questionList.size - 1) {
currentQuestionIndex++ currentQuestionIndex++
wrongAttempts = 0 wrongAttempts = 0
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
_quizState.value = _scoreReduction.value = baseScore // Reset ke 100
QuizState.QuestionLoaded(questionList[currentQuestionIndex], _totalScore.value ?: 0) _liveCurrentScore.value = baseScore
} else { startStopwatch() // Restart stopwatch
_quizState.value = QuizState.QuizFinished(_totalScore.value ?: 0) }else{
} }
} }
/** 🔢 Perhitungan Skor **/ /** Perhitungan Skor Secara Live **/
private fun calculateScore(responseTime: Int, wrongAttempts: Int): Int { private fun calculateRealTimeScore(responseTime: Int, wrongAttempts: Int): Int {
val penaltyTime = maxOf(0, responseTime - safeTime) * timePenalty val penaltyTime = max(0, responseTime - safeTime) * timePenalty
val penaltyWrong = wrongAttempts * wrongPenalty val penaltyWrong = wrongAttempts * wrongPenalty
var score = baseScore - penaltyTime - penaltyWrong var score = baseScore - penaltyTime - penaltyWrong
@ -266,6 +375,7 @@ class QuizViewModel(application: Application) : AndroidViewModel(application),
score += bonusQuick score += bonusQuick
} }
return maxOf(minScore, score) return max(minScore, score)
} }
} }

View File

@ -1,4 +0,0 @@
package com.example.lexilearn.ui.views.pQuiz.pComponentsQuiz
class AnswerShuffled {
}