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
import android.content.Context
import android.media.MediaPlayer
import android.os.Bundle
import android.util.Log
import android.widget.Toast
@ -7,6 +9,13 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
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.compose.NavHost
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.pScreening.ScreeningScreen
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() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -76,6 +158,7 @@ class MainActivity : ComponentActivity() {
}
}
setContent {
BackgroundMusicHandler()
LexiLearnTheme {
MyApp() // Menggunakan MyApp dengan tema LexiLearn
}

View File

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

View File

@ -16,13 +16,15 @@ fun generateQuestion(
answerMode: Int
): QuizQuestion {
val questionType = when (questionMode) {
1 -> listOf(QuestionType.TEXT, QuestionType.IMAGE).random()
// 1 -> listOf(QuestionType.TEXT, QuestionType.IMAGE).random()
1 -> QuestionType.TEXT
2 -> QuestionType.AUDIO
else -> QuestionType.TEXT
}
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.AUDIO -> material.enName
} ?: "Soal tidak tersedia"
@ -36,6 +38,7 @@ fun generateQuestion(
return QuizQuestion(
questionType = questionType,
question = question,
urlImage = material.image,
correctAnswer = QuizAnswer(
answerType = answerType,
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(
path = dataMat.image,
contentScale = ContentScale.Crop,
contentScale = ContentScale.Fit,
modifier = Modifier.matchParentSize()
) // 🔥 Pastikan gambar mengisi Box
}

View File

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

View File

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

View File

@ -26,6 +26,7 @@ class LearNumberViewModel(application: Application) : AndroidViewModel(applicati
override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) {
tts?.language = Locale.ENGLISH
tts?.setSpeechRate(0.5f)
_isTTSInitialized.value = true
} else {
_isTTSInitialized.value = false

View File

@ -127,7 +127,7 @@ fun NavMaterialScreen(navController: NavController, materialId: String) {
) {
FirebaseImage(
path = material.image,
contentScale = ContentScale.Crop,
contentScale = ContentScale.Fit,
modifier = Modifier.matchParentSize()
) // 🔥 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.GradientQuiz
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.ctextBlack
import com.example.lexilearn.ui.theme.ctextGray
@ -164,17 +166,38 @@ fun QuizScreen(
}
}
}
LaunchedEffect(snackbarMessage) {
snackbarMessage?.let { message ->
snackbarHostState.showSnackbar(message)
quizViewModel.onSnackbarShown()
// LaunchedEffect(snackbarMessage) {
//// snackbarMessage?.let { message ->
//// snackbarHostState.showSnackbar(message)
//// 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()
LaunchedEffect(finalScore) {
finalScore?.let { scoreFin ->
navController.navigate("resultscreening/$scoreFin") {
popUpTo("quizscreen") { inclusive = true }
if(typeQuiz=="competition"){
navController.navigate("resultscreening/$scoreFin") {
popUpTo("quizscreen") { inclusive = true }
}
}
}
}
@ -216,25 +239,25 @@ fun QuizScreen(
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Row(
Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Point: $liveCurrentScore",
color = cAccent,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Text(
text = "Time: ${formattedTime}",
color = cAccent,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
}
// Row(
// Modifier
// .fillMaxWidth()
// .padding(8.dp),
// horizontalArrangement = Arrangement.SpaceBetween
// ) {
// Text(
// text = "Point: $liveCurrentScore",
// color = cAccent,
// fontSize = 18.sp,
// fontWeight = FontWeight.Bold
// )
// Text(
// text = "Time: ${formattedTime}",
// color = cAccent,
// fontSize = 18.sp,
// fontWeight = FontWeight.Bold
// )
// }
}
MyShadowCard(
modifier = Modifier
@ -247,13 +270,12 @@ fun QuizScreen(
) {
Spacer(modifier = Modifier.height(12.dp))
if (question != null) {
if (question?.questionType == QuestionType.IMAGE) {
if (question?.questionType == QuestionType.IMAGE || question?.questionType == QuestionType.TEXT) {
FirebaseImage(
path = question!!.question,
contentScale = ContentScale.Crop,
path = question!!.urlImage,
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp)
)
} else if (question?.questionType == QuestionType.TEXT) {
Text(
text = question!!.question,
fontSize = 20.sp,
@ -261,7 +283,17 @@ fun QuizScreen(
fontWeight = FontWeight.Bold,
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(
onClick = {
quizViewModel.speakLetter(question!!.question)
@ -285,7 +317,7 @@ fun QuizScreen(
}
}
}
Spacer(modifier = Modifier.height(12.dp))
// Spacer(modifier = Modifier.height(12.dp))
FlowRow(
mainAxisSpacing = 8.dp,
crossAxisSpacing = 8.dp,

View File

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

Binary file not shown.