diff --git a/app/build.gradle b/app/build.gradle
index c2e5985e..5328fbc2 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -144,6 +144,8 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// mockito
testImplementation 'org.mockito:mockito-inline:2.21.0'
+
+ implementation "com.airbnb.android:lottie-compose:5.0.2"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f9f74246..e409325f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,6 +26,12 @@
android:supportsRtl="true"
android:theme="@style/Theme.DailyFilm"
tools:targetApi="31">
+
+
-
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/CalendarActivity.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/CalendarActivity.kt
index 3a44bef9..b8485bfe 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/CalendarActivity.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/calendar/CalendarActivity.kt
@@ -24,11 +24,10 @@ import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel
import com.boostcamp.dailyfilm.presentation.calendar.model.DateState
import com.boostcamp.dailyfilm.presentation.login.LoginActivity
import com.boostcamp.dailyfilm.presentation.playfilm.model.EditState
-import com.boostcamp.dailyfilm.presentation.searchfilm.SearchFilmActivity
import com.boostcamp.dailyfilm.presentation.searchfilm.SearchFilmComposeActivity
import com.boostcamp.dailyfilm.presentation.selectvideo.SelectVideoActivity
import com.boostcamp.dailyfilm.presentation.settings.compose.SettingComposeActivity
-import com.boostcamp.dailyfilm.presentation.totalfilm.TotalFilmActivity
+import com.boostcamp.dailyfilm.presentation.totalfilm.TotalFilmComposeActivity
import com.boostcamp.dailyfilm.presentation.trimvideo.TrimVideoActivity
import com.boostcamp.dailyfilm.presentation.uploadfilm.model.DateAndVideoModel
import com.boostcamp.dailyfilm.presentation.util.network.NetworkManager
@@ -145,7 +144,7 @@ class CalendarActivity : BaseActivity(R.layout.activity
return@setOnMenuItemClickListener true
}
startActivity(
- Intent(this@CalendarActivity, TotalFilmActivity::class.java).apply {
+ Intent(this@CalendarActivity, TotalFilmComposeActivity::class.java).apply {
putParcelableArrayListExtra(
KEY_FILM_ARRAY,
ArrayList(viewModel.filmFlow.value)
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/TotalFilmComposeActivity.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/TotalFilmComposeActivity.kt
new file mode 100644
index 00000000..d26316a1
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/TotalFilmComposeActivity.kt
@@ -0,0 +1,322 @@
+package com.boostcamp.dailyfilm.presentation.totalfilm
+
+import android.app.Activity
+import android.net.Uri
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import com.boostcamp.dailyfilm.R
+import com.google.android.exoplayer2.ExoPlayer
+import com.google.android.exoplayer2.MediaItem
+import com.google.android.exoplayer2.MediaItem.fromUri
+import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
+import com.google.android.exoplayer2.ui.StyledPlayerView
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.collectLatest
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieClipSpec
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.animateLottieCompositionAsState
+import com.airbnb.lottie.compose.rememberLottieAnimatable
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.boostcamp.dailyfilm.presentation.ui.theme.blackBlur
+import com.google.android.exoplayer2.Player
+
+@AndroidEntryPoint
+class TotalFilmComposeActivity : ComponentActivity() {
+ private val viewModel by viewModels()
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val exoPlayer = ExoPlayer.Builder(this).build().apply {
+ playWhenReady = true
+
+ addListener(object : Player.Listener {
+ override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
+ super.onMediaItemTransition(mediaItem, reason)
+ viewModel.filmArray?.get(currentMediaItemIndex)?.let { model ->
+ viewModel.setCurrentDateItem(model)
+ }
+ }
+
+ override fun onPlaybackStateChanged(playbackState: Int) {
+ if (playbackState == ExoPlayer.STATE_ENDED) {
+ viewModel.changeEndState()
+ }
+ }
+ })
+ }
+ setContent {
+ VideoPlayer(viewModel, exoPlayer)
+ PlayerControlView(viewModel)
+ }
+ }
+}
+
+@Composable
+fun VideoPlayer(viewModel: TotalFilmViewModel, exoPlayer: ExoPlayer) {
+ var videoUrl by rememberSaveable { mutableStateOf(null) }
+ val isMuted = viewModel.isMuted.collectAsStateWithLifecycle().value
+ val isEnded = viewModel.isEnded.collectAsStateWithLifecycle().value
+ val isSpeed = viewModel.isSpeed.collectAsStateWithLifecycle().value
+ if (isEnded) {
+ (LocalContext.current as Activity).finish()
+ }
+
+ exoPlayer.apply {
+ LaunchedEffect(Unit) {
+ viewModel.loadVideos()
+ viewModel.downloadedVideoUri.collectLatest {
+ it?.let { videoURL ->
+ if (videoURL != Uri.EMPTY) {
+ addMediaItem(fromUri(videoURL))
+ prepare()
+ videoUrl = videoURL
+ } else {
+ return@collectLatest
+ }
+ }
+ }
+ }
+ setPlaybackSpeed(isSpeed.speed)
+ volume = when (isMuted) {
+ true -> {
+ 0.0f
+ }
+
+ false -> {
+ 0.5f
+ }
+ }
+ }
+ videoUrl?.let { VideoView(it, exoPlayer) }
+}
+
+@Composable
+fun VideoView(videoUrl: Uri, exoPlayer: ExoPlayer) {
+
+ exoPlayer.currentMediaItem?.let {
+ val context = LocalContext.current
+ val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
+ DisposableEffect(
+ AndroidView(factory = {
+ StyledPlayerView(context).apply {
+ player = exoPlayer
+ resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
+ useController = false
+ }.apply {
+ setOnClickListener {
+ if (exoPlayer.isPlaying) {
+ exoPlayer.pause()
+ } else {
+ exoPlayer.play()
+ }
+ }
+ }
+ })
+ ) {
+ val observer = LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_PAUSE -> {
+ exoPlayer.pause()
+ }
+
+ Lifecycle.Event.ON_RESUME -> {
+ exoPlayer.play()
+ }
+
+ else -> {}
+ }
+ }
+ val lifecycle = lifecycleOwner.value.lifecycle
+ lifecycle.addObserver(observer)
+
+ onDispose {
+ exoPlayer.release()
+ lifecycle.removeObserver(observer)
+ }
+ }
+ }
+}
+
+@Composable
+fun PlayerControlView(
+ viewModel: TotalFilmViewModel,
+) {
+ val isMuted = viewModel.isMuted.collectAsStateWithLifecycle().value
+ val curSpeed = viewModel.isSpeed.collectAsStateWithLifecycle().value
+ val isContentShowed = viewModel.isContentShowed.collectAsStateWithLifecycle().value
+ val dateModel = viewModel.currentDateItem.collectAsStateWithLifecycle().value
+
+ val soundComposition by rememberLottieComposition(
+ LottieCompositionSpec.Asset(stringResource(R.string.lottie_sound))
+ )
+ val textComposition by rememberLottieComposition(
+ LottieCompositionSpec.RawRes(resId = R.raw.lottie_textstate)
+ )
+ val soundAnimatable = rememberLottieAnimatable()
+
+ var checked by remember { mutableStateOf(true) }
+ var isPlaying by remember { mutableStateOf(true) }
+ val progress by animateLottieCompositionAsState(
+ composition = textComposition,
+ restartOnPlay = false,
+ isPlaying = isPlaying,
+ speed = if (isContentShowed) 1f else -1f,
+ clipSpec = LottieClipSpec.Progress(0.25f, 0.67f)
+ )
+ LaunchedEffect(isMuted) {
+ soundAnimatable.animate(
+ composition = soundComposition,
+ clipSpec = if (isMuted) {
+ LottieClipSpec.Progress(0.0f, 0.5f)
+ } else {
+ LottieClipSpec.Progress(0.5f, 1.0f)
+ },
+ )
+ }
+ LaunchedEffect(progress) {
+ if (progress == 0.67f) {
+ isPlaying = false
+ checked = true
+ }
+ if (progress == 0.25f && !checked) {
+ isPlaying = false
+ checked = false
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Transparent)
+ .padding(dimensionResource(id = R.dimen.normal_100))
+ ) {
+ Box(
+ modifier = Modifier
+ .background(blackBlur, RoundedCornerShape(4.dp))
+ .align(Alignment.TopStart)
+ .padding(start = 6.dp, end = 6.dp, top = 4.dp, bottom = 4.dp)
+ .height(dimensionResource(id = R.dimen.normal_175))
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = stringResource(
+ R.string.date, dateModel!!.year, dateModel.month, dateModel.day
+ ),
+ color = Color.White,
+ fontSize = 16.sp
+ )
+ }
+
+ Row(
+ modifier = Modifier.align(Alignment.TopEnd),
+ horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.normal_100))
+ ) {
+ Box(
+ modifier = Modifier
+ .background(blackBlur, RoundedCornerShape(4.dp))
+ .padding(start = 6.dp, end = 6.dp, top = 4.dp, bottom = 4.dp)
+ .size(dimensionResource(id = R.dimen.normal_175))
+ .clickable { viewModel.changeSpeedState() }
+ ) {
+ Image(
+ modifier = Modifier.align(Alignment.Center),
+ painter = painterResource(id = R.drawable.ic_fast),
+ contentDescription = stringResource(R.string.control_speed)
+ )
+ }
+ Box(
+ modifier = Modifier
+ .background(blackBlur, RoundedCornerShape(4.dp))
+ .padding(start = 6.dp, end = 6.dp, top = 4.dp, bottom = 4.dp)
+ .height(dimensionResource(id = R.dimen.normal_175))
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = curSpeed.toString(),
+ color = Color.White,
+ fontSize = 16.sp
+ )
+ }
+ LottieAnimation(
+ composition = soundComposition,
+ progress = soundAnimatable.progress,
+ modifier = Modifier
+ .background(blackBlur, RoundedCornerShape(4.dp))
+ .padding(4.dp)
+ .size(dimensionResource(id = R.dimen.normal_175))
+ .clickable { viewModel.changeMuteState() })
+ }
+
+ AnimatedVisibility(
+ visible = isContentShowed,
+ modifier = Modifier.align(Alignment.Center),
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Text(
+ text = dateModel!!.text ?: "",
+ modifier = Modifier
+ .background(
+ Color.Black.copy(alpha = 0.5f), RoundedCornerShape(4.dp)
+ )
+ .padding(4.dp),
+ color = Color.White,
+ fontSize = 16.sp
+ )
+ }
+
+ LottieAnimation(
+ composition = textComposition,
+ progress = progress,
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .background(blackBlur, RoundedCornerShape(50.dp))
+ .size(dimensionResource(id = R.dimen.large_200))
+ .padding(4.dp)
+ .clickable {
+ isPlaying = true
+ viewModel.changeShowState()
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/TotalFilmViewModel.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/TotalFilmViewModel.kt
index 69913a36..f87d05b6 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/TotalFilmViewModel.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/TotalFilmViewModel.kt
@@ -30,32 +30,28 @@ class TotalFilmViewModel @Inject constructor(
private val _downloadedVideoUri = MutableSharedFlow()
val downloadedVideoUri: SharedFlow = _downloadedVideoUri.asSharedFlow()
- private val _isContentShowed = MutableLiveData(true)
- val isContentShowed: LiveData get() = _isContentShowed
+ private val _isContentShowed = MutableStateFlow(true)
+ val isContentShowed: StateFlow get() = _isContentShowed
- private val _isMuted = MutableLiveData(false)
- val isMuted: LiveData get() = _isMuted
+ private val _isMuted = MutableStateFlow(false)
+ val isMuted: StateFlow get() = _isMuted
private val _isEnded = MutableStateFlow(false)
val isEnded: StateFlow get() = _isEnded.asStateFlow()
- private val _isSpeed = MutableLiveData(SpeedState.values()[speedIndex ?: 2])
- val isSpeed: LiveData get() = _isSpeed
+ private val _isSpeed = MutableStateFlow(SpeedState.values()[speedIndex ?: 2])
+ val isSpeed: StateFlow get() = _isSpeed
fun setCurrentDateItem(dateModel: DateModel) {
_currentDateItem.value = dateModel
}
- init {
- loadVideos()
- }
-
fun changeShowState() {
- _isContentShowed.value = _isContentShowed.value?.not()
+ _isContentShowed.value = _isContentShowed.value.not()
}
fun changeMuteState() {
- _isMuted.value = _isMuted.value?.not()
+ _isMuted.value = _isMuted.value.not()
}
fun changeEndState() {
@@ -74,7 +70,7 @@ class TotalFilmViewModel @Inject constructor(
}
}
- private fun loadVideos() {
+ fun loadVideos() {
viewModelScope.launch {
filmArray?.forEach { dateModel ->
yield()
@@ -103,11 +99,13 @@ class TotalFilmViewModel @Inject constructor(
}
}
}
+
is Result.Error -> {}
}
}
}
}
+
is Result.Error -> {}
}
}
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/ui/theme/Color.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/ui/theme/Color.kt
new file mode 100644
index 00000000..3127a86c
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.boostcamp.dailyfilm.presentation.totalfilm.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/ui/theme/Theme.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/ui/theme/Theme.kt
new file mode 100644
index 00000000..e3b61347
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/ui/theme/Theme.kt
@@ -0,0 +1,70 @@
+package com.boostcamp.dailyfilm.presentation.totalfilm.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun DailyFilmTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = colorScheme.primary.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/ui/theme/Type.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/ui/theme/Type.kt
new file mode 100644
index 00000000..7f00efe9
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/totalfilm/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.boostcamp.dailyfilm.presentation.totalfilm.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
index a3d36b3f..37c506cf 100644
--- a/app/src/main/res/values-en/strings.xml
+++ b/app/src/main/res/values-en/strings.xml
@@ -38,4 +38,6 @@
DailyFilm Logo
Google logo
Sign in with Google
+ sound_lottie.json
+ control speed
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 9b52d544..71eda12d 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -39,4 +39,7 @@
DailyFilm Logo
Google logo
Sign in with Google
+ sound_lottie.json
+ control speed
+
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 1eea83cb..31285191 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -38,4 +38,7 @@
DailyFilm Logo
Google logo
Sign in with Google
+ sound_lottie.json
+ control speed
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7f675de7..9338514c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -54,4 +54,6 @@
설정 화면
TotalComposeActivity
Failed Google Login
+ sound_lottie.json
+ 속도 조절
\ No newline at end of file