This commit is contained in:
DimazzP 2025-05-15 19:26:16 +07:00
parent 9f049952e8
commit a2ff149f23
16 changed files with 357 additions and 90 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,7 @@
package com.example.lexilearn package com.example.lexilearn
import android.content.Context
import android.media.MediaPlayer
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
@ -7,6 +9,13 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@ -30,7 +39,80 @@ import com.example.lexilearn.ui.views.pRegister.RegisterScreen
import com.example.lexilearn.ui.views.pResultScreening.ResultScreeningScreen import com.example.lexilearn.ui.views.pResultScreening.ResultScreeningScreen
import com.example.lexilearn.ui.views.pScreening.ScreeningScreen import com.example.lexilearn.ui.views.pScreening.ScreeningScreen
import com.example.lexilearn.ui.views.pSplashcreen.SplashScreen import com.example.lexilearn.ui.views.pSplashcreen.SplashScreen
class BackgroundMusicPlayer(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null
fun play() {
// Stop existing player if any
stop()
// Create new media player
mediaPlayer = MediaPlayer.create(context, R.raw.music_background).apply {
isLooping = true
setVolume(0.2f, 0.2f) // Set volume to 30%
start()
}
}
fun pause() {
mediaPlayer?.pause()
}
fun resume() {
mediaPlayer?.start()
}
fun stop() {
mediaPlayer?.let {
if (it.isPlaying) {
it.stop()
}
it.release()
}
mediaPlayer = null
}
}
@Composable
fun BackgroundMusicHandler() {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val musicPlayer = remember { BackgroundMusicPlayer(context) }
// Observe lifecycle and control music
DisposableEffect(lifecycleOwner) {
val observer = object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
musicPlayer.play()
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause() {
musicPlayer.pause()
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
musicPlayer.stop()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
// Initial play
musicPlayer.play()
// Cleanup
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
musicPlayer.stop()
}
}
// No UI component needed
return
}
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -76,6 +158,7 @@ class MainActivity : ComponentActivity() {
} }
} }
setContent { setContent {
BackgroundMusicHandler()
LexiLearnTheme { LexiLearnTheme {
MyApp() // Menggunakan MyApp dengan tema LexiLearn MyApp() // Menggunakan MyApp dengan tema LexiLearn
} }

View File

@ -3,5 +3,6 @@ package com.example.lexilearn.data.model
data class QuizQuestion( data class QuizQuestion(
val questionType: QuestionType, // TEXT, IMAGE, AUDIO val questionType: QuestionType, // TEXT, IMAGE, AUDIO
val question: String, // Bisa teks, URL gambar, atau teks untuk TTS val question: String, // Bisa teks, URL gambar, atau teks untuk TTS
val urlImage: String,
val correctAnswer: QuizAnswer // Jawaban yang benar val correctAnswer: QuizAnswer // Jawaban yang benar
) )

View File

@ -16,13 +16,15 @@ fun generateQuestion(
answerMode: Int answerMode: Int
): QuizQuestion { ): QuizQuestion {
val questionType = when (questionMode) { val questionType = when (questionMode) {
1 -> listOf(QuestionType.TEXT, QuestionType.IMAGE).random() // 1 -> listOf(QuestionType.TEXT, QuestionType.IMAGE).random()
1 -> QuestionType.TEXT
2 -> QuestionType.AUDIO 2 -> QuestionType.AUDIO
else -> QuestionType.TEXT else -> QuestionType.TEXT
} }
val question = when (questionType) { val question = when (questionType) {
QuestionType.TEXT -> listOf(material.idName, material.idDescription).random() // QuestionType.TEXT -> listOf(material.idName, material.idDescription).random()
QuestionType.TEXT -> material.idName
QuestionType.IMAGE -> material.image QuestionType.IMAGE -> material.image
QuestionType.AUDIO -> material.enName QuestionType.AUDIO -> material.enName
} ?: "Soal tidak tersedia" } ?: "Soal tidak tersedia"
@ -36,6 +38,7 @@ fun generateQuestion(
return QuizQuestion( return QuizQuestion(
questionType = questionType, questionType = questionType,
question = question, question = question,
urlImage = material.image,
correctAnswer = QuizAnswer( correctAnswer = QuizAnswer(
answerType = answerType, answerType = answerType,
correctWord = material.enName correctWord = material.enName

View File

@ -0,0 +1,70 @@
package com.example.lexilearn.ui.components
import LottieAnimationComponent
import android.speech.tts.TextToSpeech
import android.speech.tts.TextToSpeech.OnInitListener
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import kotlinx.coroutines.delay
import java.util.*
@Composable
fun RightAnswerDialog(onDismiss: () -> Unit) {
// Inisialisasi TextToSpeech
val context = LocalContext.current
val tts = remember {
TextToSpeech(context, OnInitListener { status ->
})
}
tts.language = Locale("id", "ID")
// Ketika dialog muncul, TTS akan berbicara "Jawaban Salah"
LaunchedEffect(Unit) {
tts.speak("Jawaban Benar", TextToSpeech.QUEUE_FLUSH, null, null)
delay(2000) // Delay 3 detik
onDismiss()
}
Dialog(onDismissRequest = onDismiss) {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
color = Color.Transparent,
shape = MaterialTheme.shapes.medium
) {
LottieAnimationComponent(
assetName = "right_answer.json",
playOnce = true,
modifier = Modifier
)
}
}
DisposableEffect(context) {
onDispose {
tts.stop()
tts.shutdown()
}
}
}

View File

@ -0,0 +1,71 @@
package com.example.lexilearn.ui.components
import LottieAnimationComponent
import android.speech.tts.TextToSpeech
import android.speech.tts.TextToSpeech.OnInitListener
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import kotlinx.coroutines.delay
import java.util.*
@Composable
fun WrongAnswerDialog(onDismiss: () -> Unit) {
// Inisialisasi TextToSpeech
val context = LocalContext.current
val tts = remember {
TextToSpeech(context, OnInitListener { status ->
})
}
tts.language = Locale("id", "ID")
// Ketika dialog muncul, TTS akan berbicara "Jawaban Salah"
LaunchedEffect(Unit) {
tts.speak("Jawaban Salah", TextToSpeech.QUEUE_FLUSH, null, null)
delay(2000) // Delay 3 detik
onDismiss()
}
Dialog(onDismissRequest = onDismiss) {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
color = Color.Transparent,
shape = MaterialTheme.shapes.medium
) {
LottieAnimationComponent(
assetName = "wrong_answer.json",
playOnce = true,
modifier = Modifier
)
}
}
DisposableEffect(context) {
onDispose {
tts.stop()
tts.shutdown()
}
}
}

View File

@ -107,7 +107,7 @@ fun DetailMaterialScreen(
) { ) {
FirebaseImage( FirebaseImage(
path = dataMat.image, path = dataMat.image,
contentScale = ContentScale.Crop, contentScale = ContentScale.Fit,
modifier = Modifier.matchParentSize() modifier = Modifier.matchParentSize()
) // 🔥 Pastikan gambar mengisi Box ) // 🔥 Pastikan gambar mengisi Box
} }

View File

@ -14,65 +14,66 @@ import kotlinx.coroutines.flow.StateFlow
import java.util.* import java.util.*
class DetailMaterialViewModel(application: Application) : AndroidViewModel(application), class DetailMaterialViewModel(application: Application) : AndroidViewModel(application),
TextToSpeech.OnInitListener { TextToSpeech.OnInitListener {
private var tts: TextToSpeech? = null private var tts: TextToSpeech? = null
private val _isTTSInitialized = MutableLiveData(false) private val _isTTSInitialized = MutableLiveData(false)
val isTTSInitialized: LiveData<Boolean> = _isTTSInitialized val isTTSInitialized: LiveData<Boolean> = _isTTSInitialized
val alphabetList = generateNumberList() val alphabetList = generateNumberList()
private val quizRepository = QuizRepository() private val quizRepository = QuizRepository()
private val repository = MaterialRepository(application) private val repository = MaterialRepository(application)
private val _materialList = MutableStateFlow<List<MaterialDataModel>>(emptyList()) private val _materialList = MutableStateFlow<List<MaterialDataModel>>(emptyList())
val materialList: StateFlow<List<MaterialDataModel>> = _materialList val materialList: StateFlow<List<MaterialDataModel>> = _materialList
private val _textTitle = MutableLiveData("Hello, Jetpack Compose!") private val _textTitle = MutableLiveData("Hello, Jetpack Compose!")
val textTitle: LiveData<String> = _textTitle val textTitle: LiveData<String> = _textTitle
fun fetchMaterial(materialListData: List<MaterialDataModel>) { fun fetchMaterial(materialListData: List<MaterialDataModel>) {
_materialList.value = materialListData _materialList.value = materialListData
if (materialListData.isNotEmpty()) { if (materialListData.isNotEmpty()) {
if (materialListData[0].category == "animal") { if (materialListData[0].category == "animal") {
_textTitle.value = "hewan" _textTitle.value = "hewan"
} else if (materialListData[0].category == "limb") { } else if (materialListData[0].category == "limb") {
_textTitle.value = "anggota tubuh" _textTitle.value = "anggota tubuh"
} else if (materialListData[0].category == "house") { } else if (materialListData[0].category == "house") {
_textTitle.value = "bagian rumah" _textTitle.value = "bagian rumah"
} else if (materialListData[0].category == "family") { } else if (materialListData[0].category == "family") {
_textTitle.value = "anggota keluarga" _textTitle.value = "anggota keluarga"
}
} }
} }
}
fun randomMaterialModel(): List<MaterialDataModel> {
return _materialList.value.shuffled() fun randomMaterialModel(): List<MaterialDataModel> {
} return _materialList.value.shuffled()
}
init {
tts = TextToSpeech(application, this) init {
} tts = TextToSpeech(application, this)
}
override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) { override fun onInit(status: Int) {
tts?.language = Locale.ENGLISH if (status == TextToSpeech.SUCCESS) {
_isTTSInitialized.value = true tts?.language = Locale.ENGLISH
} else { _isTTSInitialized.value = true
_isTTSInitialized.value = false tts?.setSpeechRate(0.5f)
} } else {
} _isTTSInitialized.value = false
}
fun speakLetter(letter: String) { }
if (_isTTSInitialized.value == true) {
tts?.speak(letter, TextToSpeech.QUEUE_FLUSH, null, null) fun speakLetter(letter: String) {
} if (_isTTSInitialized.value == true) {
} tts?.speak(letter, TextToSpeech.QUEUE_FLUSH, null, null)
}
override fun onCleared() { }
tts?.stop()
tts?.shutdown() override fun onCleared() {
super.onCleared() tts?.stop()
} tts?.shutdown()
super.onCleared()
}
} }

View File

@ -41,6 +41,7 @@ import androidx.constraintlayout.compose.Dimension
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.example.lexilearn.R import com.example.lexilearn.R
import com.example.lexilearn.data.model.UserDataModel
import com.example.lexilearn.ui.components.AutoSizeText import com.example.lexilearn.ui.components.AutoSizeText
import com.example.lexilearn.ui.components.ButtonHome import com.example.lexilearn.ui.components.ButtonHome
import com.example.lexilearn.ui.components.DialogProfile import com.example.lexilearn.ui.components.DialogProfile
@ -418,15 +419,15 @@ fun HomeScreen(navController: NavController, viewModel: HomeViewModel = viewMode
} }
} }
} }
if(userData.value!=null){ // if(userData.value!=null){
DialogProfile( DialogProfile(
showDialog = showDialogProfile.value, showDialog = showDialogProfile.value,
repository = viewModel.userRepository, repository = viewModel.userRepository,
userData = userData.value!! userData = userData.value ?: UserDataModel(name = "User", age = 6)
) { ) {
viewModel.showHiddenDialog() viewModel.showHiddenDialog()
viewModel.getUserData() viewModel.getUserData()
} }
} // }
} }
} }

View File

@ -28,6 +28,7 @@ class LearAlphabetViewModel(application: Application) : AndroidViewModel(applica
if (status == TextToSpeech.SUCCESS) { if (status == TextToSpeech.SUCCESS) {
tts?.language = Locale.ENGLISH tts?.language = Locale.ENGLISH
_isTTSInitialized.value = true _isTTSInitialized.value = true
tts?.setSpeechRate(0.5f)
} else { } else {
_isTTSInitialized.value = false _isTTSInitialized.value = false
} }

View File

@ -26,6 +26,7 @@ class LearNumberViewModel(application: Application) : AndroidViewModel(applicati
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

View File

@ -127,7 +127,7 @@ fun NavMaterialScreen(navController: NavController, materialId: String) {
) { ) {
FirebaseImage( FirebaseImage(
path = material.image, path = material.image,
contentScale = ContentScale.Crop, contentScale = ContentScale.Fit,
modifier = Modifier.matchParentSize() modifier = Modifier.matchParentSize()
) // 🔥 Pastikan gambar mengisi Box ) // 🔥 Pastikan gambar mengisi Box
} }

View File

@ -59,6 +59,8 @@ 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.components.RightAnswerDialog
import com.example.lexilearn.ui.components.WrongAnswerDialog
import com.example.lexilearn.ui.theme.cAccent import com.example.lexilearn.ui.theme.cAccent
import com.example.lexilearn.ui.theme.ctextBlack import com.example.lexilearn.ui.theme.ctextBlack
import com.example.lexilearn.ui.theme.ctextGray import com.example.lexilearn.ui.theme.ctextGray
@ -164,17 +166,38 @@ fun QuizScreen(
} }
} }
} }
LaunchedEffect(snackbarMessage) { // LaunchedEffect(snackbarMessage) {
snackbarMessage?.let { message -> //// snackbarMessage?.let { message ->
snackbarHostState.showSnackbar(message) //// snackbarHostState.showSnackbar(message)
quizViewModel.onSnackbarShown() //// quizViewModel.onSnackbarShown()
//// }
// if (snackbarMessage!=null) {
// RightAnswerDialog(
// message = dialogMessage,
// onDismiss = { quizViewModel._showDialog.value = false }
// )
// }
//
// }
if (snackbarMessage != null) {
if(snackbarMessage == "Jawaban Benar"){
RightAnswerDialog(
onDismiss = { quizViewModel.onSnackbarShown() } // Setelah dialog ditutup, reset snackbarMessage
)
}else{
WrongAnswerDialog(
onDismiss = { quizViewModel.onSnackbarShown() } // Setelah dialog ditutup, reset snackbarMessage
)
} }
} }
val finalScore by quizViewModel.finalScore.collectAsState() val finalScore by quizViewModel.finalScore.collectAsState()
LaunchedEffect(finalScore) { LaunchedEffect(finalScore) {
finalScore?.let { scoreFin -> finalScore?.let { scoreFin ->
navController.navigate("resultscreening/$scoreFin") { if(typeQuiz=="competition"){
popUpTo("quizscreen") { inclusive = true } navController.navigate("resultscreening/$scoreFin") {
popUpTo("quizscreen") { inclusive = true }
}
} }
} }
} }
@ -216,25 +239,25 @@ fun QuizScreen(
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Row( // Row(
Modifier // Modifier
.fillMaxWidth() // .fillMaxWidth()
.padding(8.dp), // .padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween // horizontalArrangement = Arrangement.SpaceBetween
) { // ) {
Text( // Text(
text = "Point: $liveCurrentScore", // text = "Point: $liveCurrentScore",
color = cAccent, // color = cAccent,
fontSize = 18.sp, // fontSize = 18.sp,
fontWeight = FontWeight.Bold // fontWeight = FontWeight.Bold
) // )
Text( // Text(
text = "Time: ${formattedTime}", // text = "Time: ${formattedTime}",
color = cAccent, // color = cAccent,
fontSize = 18.sp, // fontSize = 18.sp,
fontWeight = FontWeight.Bold // fontWeight = FontWeight.Bold
) // )
} // }
} }
MyShadowCard( MyShadowCard(
modifier = Modifier modifier = Modifier
@ -247,13 +270,12 @@ fun QuizScreen(
) { ) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
if (question != null) { if (question != null) {
if (question?.questionType == QuestionType.IMAGE) { if (question?.questionType == QuestionType.IMAGE || question?.questionType == QuestionType.TEXT) {
FirebaseImage( FirebaseImage(
path = question!!.question, path = question!!.urlImage,
contentScale = ContentScale.Crop, contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp) modifier = Modifier.size(160.dp)
) )
} else if (question?.questionType == QuestionType.TEXT) {
Text( Text(
text = question!!.question, text = question!!.question,
fontSize = 20.sp, fontSize = 20.sp,
@ -261,7 +283,17 @@ fun QuizScreen(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(12.dp) modifier = Modifier.padding(12.dp)
) )
} else if (question?.questionType == QuestionType.AUDIO) { }
// else if (question?.questionType == QuestionType.TEXT) {
// Text(
// text = question!!.question,
// fontSize = 20.sp,
// color = ctextBlack,
// fontWeight = FontWeight.Bold,
// modifier = Modifier.padding(12.dp)
// )
// }
else if (question?.questionType == QuestionType.AUDIO) {
Button( Button(
onClick = { onClick = {
quizViewModel.speakLetter(question!!.question) quizViewModel.speakLetter(question!!.question)
@ -285,7 +317,7 @@ fun QuizScreen(
} }
} }
} }
Spacer(modifier = Modifier.height(12.dp)) // Spacer(modifier = Modifier.height(12.dp))
FlowRow( FlowRow(
mainAxisSpacing = 8.dp, mainAxisSpacing = 8.dp,
crossAxisSpacing = 8.dp, crossAxisSpacing = 8.dp,

View File

@ -149,6 +149,7 @@ class QuizViewModel(application: Application) : AndroidViewModel(application),
if (checkData) { if (checkData) {
triggerSnackbar("Jawaban Benar") triggerSnackbar("Jawaban Benar")
submitAnswer(true) submitAnswer(true)
delay(2000)
incrementIndexQuiz() incrementIndexQuiz()
} else { } else {
submitAnswer(false) submitAnswer(false)

Binary file not shown.