update live score
This commit is contained in:
parent
b500df7296
commit
217f587965
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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":[]}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.VolumeUp,
|
imageVector = Icons.Filled.VolumeUp,
|
||||||
contentDescription = "Speaker Icon",
|
contentDescription = "Speaker Icon",
|
||||||
tint = Color.Black,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(200.dp)
|
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
|
||||||
|
|
|
@ -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 **/
|
||||||
|
private fun startStopwatch() {
|
||||||
|
isRunning = true
|
||||||
|
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) {
|
fun submitAnswer(isCorrect: Boolean) {
|
||||||
val responseTime =
|
|
||||||
((System.currentTimeMillis() - startTime) / 1000).toInt() // Hitung waktu otomatis
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
_totalScore.value += finalScore // Tambahkan ke total skor
|
||||||
|
viewModelScope.launch {
|
||||||
|
delay(1000) // Tampilkan skor selama 1 detik sebelum pindah ke soal berikutnya
|
||||||
moveToNextQuestion()
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
package com.example.lexilearn.ui.views.pQuiz.pComponentsQuiz
|
|
||||||
|
|
||||||
class AnswerShuffled {
|
|
||||||
}
|
|
Loading…
Reference in New Issue