diff --git a/app/build.gradle b/app/build.gradle
index 36b14651..0c25ddf4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -69,6 +69,7 @@ dependencies {
def pagingVersion = "3.1.1"
def glide_version = "4.14.2"
def glide_compose_version = "1.0.0-alpha.1"
+ def lottieComposeVersion = "6.0.0"
// Compose
def composeBom = platform('androidx.compose:compose-bom:2023.04.01')
@@ -88,6 +89,8 @@ dependencies {
implementation 'androidx.compose.runtime:runtime-livedata'
// collectAsStateWithLifecycle()
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.1"
+ // Compose Text Effect
+ implementation "me.saket.extendedspans:extendedspans:1.3.0"
// Navigation
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
@@ -121,6 +124,8 @@ dependencies {
implementation "androidx.paging:paging-compose:$pagingComposeVersion"
// Exoplayer
+ implementation 'androidx.media3:media3-exoplayer:1.1.0-alpha01'
+ implementation "androidx.media3:media3-ui:1.1.0-alpha01"
implementation 'com.google.android.exoplayer:exoplayer:2.18.7'
// Coordinator-layout
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
@@ -129,6 +134,7 @@ dependencies {
implementation 'com.arthenica:mobile-ffmpeg-min-gpl:4.4'
// lottie
implementation 'com.airbnb.android:lottie:5.0.2'
+ implementation "com.airbnb.android:lottie-compose:$lottieComposeVersion"
// dataStore
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.datastore:datastore-core:1.0.0")
@@ -140,6 +146,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 946e1d9d..acf81103 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">
+
+
@@ -38,6 +44,9 @@
+
@@ -60,7 +69,8 @@
-
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 e4c123bf..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
@@ -27,7 +27,7 @@ import com.boostcamp.dailyfilm.presentation.playfilm.model.EditState
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
@@ -144,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/playfilm/PlayFilmFragment.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/PlayFilmFragment.kt
index 8e47acb2..f3576578 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/PlayFilmFragment.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/PlayFilmFragment.kt
@@ -14,13 +14,9 @@ import androidx.fragment.app.viewModels
import com.boostcamp.dailyfilm.R
import com.boostcamp.dailyfilm.databinding.FragmentPlayFilmBinding
import com.boostcamp.dailyfilm.presentation.BaseFragment
-import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity
-import com.boostcamp.dailyfilm.presentation.calendar.DateFragment.Companion.KEY_CALENDAR_INDEX
import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel
import com.boostcamp.dailyfilm.presentation.util.network.NetworkManager
import com.boostcamp.dailyfilm.presentation.util.network.NetworkState
-import com.boostcamp.dailyfilm.presentation.util.PlayState
-import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
@@ -97,7 +93,7 @@ class PlayFilmFragment : BaseFragment(R.layout.fragment
}
}*/
- viewModel.playState.observe(viewLifecycleOwner) {
+ /*viewModel.playState.observe(viewLifecycleOwner) {
when(it) {
is PlayState.Uninitialized -> {}
is PlayState.Loading -> {}
@@ -121,7 +117,7 @@ class PlayFilmFragment : BaseFragment(R.layout.fragment
}
}
}
- }
+ }*/
}
private fun initListener() {
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/PlayFilmViewModel.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/PlayFilmViewModel.kt
index c9fd7b9b..d13f5ca6 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/PlayFilmViewModel.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/PlayFilmViewModel.kt
@@ -2,16 +2,24 @@ package com.boostcamp.dailyfilm.presentation.playfilm
import android.net.Uri
import android.util.Log
-import androidx.lifecycle.*
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import com.boostcamp.dailyfilm.data.delete.DeleteFilmRepository
import com.boostcamp.dailyfilm.data.model.Result
import com.boostcamp.dailyfilm.data.playfilm.PlayFilmRepository
import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel
-import com.boostcamp.dailyfilm.presentation.util.network.NetworkManager
-import com.boostcamp.dailyfilm.presentation.util.network.NetworkState
+import com.boostcamp.dailyfilm.presentation.playfilm.base.ContentShowState
+import com.boostcamp.dailyfilm.presentation.playfilm.base.MuteState
import com.boostcamp.dailyfilm.presentation.util.PlayState
import com.boostcamp.dailyfilm.presentation.util.UiState
+import com.boostcamp.dailyfilm.presentation.util.network.NetworkManager
+import com.boostcamp.dailyfilm.presentation.util.network.NetworkState
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -31,14 +39,14 @@ class PlayFilmViewModel @Inject constructor(
private val _videoUri = MutableLiveData()
val videoUri: LiveData get() = _videoUri
- private val _isContentShowed = MutableLiveData(true)
- val isContentShowed: LiveData get() = _isContentShowed
+ private val _contentShowState = MutableStateFlow(ContentShowState(true))
+ val contentShowState: StateFlow get() = _contentShowState
- private val _isMuted = MutableLiveData(false)
- val isMuted: LiveData get() = _isMuted
+ private val _muteState = MutableStateFlow(MuteState(false))
+ val muteState: StateFlow get() = _muteState
- private val _playState = MutableLiveData(PlayState.Uninitialized)
- val playState: LiveData get() = _playState
+ private val _playState = MutableStateFlow(PlayState.Uninitialized)
+ val playState: StateFlow get() = _playState
private val _networkState = MutableLiveData(NetworkManager.checkNetwork())
val networkState: LiveData get() = _networkState
@@ -46,12 +54,18 @@ class PlayFilmViewModel @Inject constructor(
private val _isNetworkConnectShowed = MutableLiveData(true)
val isNetworkConnectShowed: LiveData get() = _isNetworkConnectShowed
- private val _isProgressed = MutableLiveData(false)
- val isProgressed: LiveData get() = _isProgressed
+ private val _isProgressed = MutableStateFlow(false)
+ val isProgressed: StateFlow get() = _isProgressed
+
+ private val _openDialog = MutableStateFlow(false)
+ val openDialog : StateFlow get() = _openDialog
init {
loadVideo()
}
+ fun setDialog(value: Boolean) {
+ _openDialog.value = value
+ }
private fun checkNetwork() {
_networkState.value = NetworkManager.checkNetwork()
@@ -62,14 +76,6 @@ class PlayFilmViewModel @Inject constructor(
_text.value = text
}
- fun changeShowState() {
- _isContentShowed.value = _isContentShowed.value?.not()
- }
-
- fun changeMuteState() {
- _isMuted.value = _isMuted.value?.not()
- }
-
fun setNetworkState(state: NetworkState) {
viewModelScope.launch {
// isNetworkConnected 는 연결 여부를 떠나 Playing 중이면 보여 주지 않는다.
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/adapter/PlayFilmPageAdapter.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/adapter/PlayFilmPageAdapter.kt
index 365e71f7..54365b36 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/adapter/PlayFilmPageAdapter.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/adapter/PlayFilmPageAdapter.kt
@@ -4,6 +4,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel
import com.boostcamp.dailyfilm.presentation.playfilm.PlayFilmFragment
+import com.boostcamp.dailyfilm.presentation.playfilm.compose.PlayFilmComposeFragment
class PlayFilmPageAdapter(
private val dateList: ArrayList,
@@ -13,7 +14,7 @@ class PlayFilmPageAdapter(
override fun getItemCount(): Int = dateList.size
- override fun createFragment(position: Int): PlayFilmFragment {
- return PlayFilmFragment.newInstance(dateList[position])
+ override fun createFragment(position: Int): PlayFilmComposeFragment {
+ return PlayFilmComposeFragment.newInstance(dateList[position])
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/base/ContentShowState.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/base/ContentShowState.kt
new file mode 100644
index 00000000..7ca3f360
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/base/ContentShowState.kt
@@ -0,0 +1,18 @@
+package com.boostcamp.dailyfilm.presentation.playfilm.base
+
+import com.airbnb.lottie.compose.LottieClipSpec
+
+class ContentShowState(init: Boolean): LottieState(init) {
+ override val clipSpec: LottieClipSpec
+ get() = if (state) {
+ LottieClipSpec.Progress(START, MID)
+ } else {
+ LottieClipSpec.Progress(MID, FINISH)
+ }
+
+ companion object {
+ const val START = 0.67f
+ const val MID = 0.25f
+ const val FINISH = 0.67f
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/base/LottieState.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/base/LottieState.kt
new file mode 100644
index 00000000..08202d5a
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/base/LottieState.kt
@@ -0,0 +1,16 @@
+package com.boostcamp.dailyfilm.presentation.playfilm.base
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.airbnb.lottie.compose.LottieClipSpec
+
+abstract class LottieState(initial: Boolean) {
+ var state by mutableStateOf(initial)
+
+ abstract val clipSpec: LottieClipSpec
+
+ fun updateState() {
+ state = !state
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/base/MuteState.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/base/MuteState.kt
new file mode 100644
index 00000000..5e4bf857
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/base/MuteState.kt
@@ -0,0 +1,19 @@
+package com.boostcamp.dailyfilm.presentation.playfilm.base
+
+import com.airbnb.lottie.compose.LottieClipSpec
+
+class MuteState(init: Boolean): LottieState(init) {
+
+ override val clipSpec: LottieClipSpec
+ get() = if (state) {
+ LottieClipSpec.Progress(START, MID)
+ } else {
+ LottieClipSpec.Progress(MID, FINISH)
+ }
+
+ companion object {
+ const val START = 0.0f
+ const val MID = 0.5f
+ const val FINISH = 1.0f
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/compose/PlayFilmComposeFragment.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/compose/PlayFilmComposeFragment.kt
new file mode 100644
index 00000000..61405e27
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/compose/PlayFilmComposeFragment.kt
@@ -0,0 +1,192 @@
+package com.boostcamp.dailyfilm.presentation.playfilm.compose
+
+import android.annotation.SuppressLint
+import android.app.Activity.RESULT_OK
+import android.content.Intent
+import android.net.ConnectivityManager
+import android.net.Network
+import android.os.Bundle
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
+import com.boostcamp.dailyfilm.R
+import com.boostcamp.dailyfilm.databinding.FragmentPlayFilmComposeBinding
+import com.boostcamp.dailyfilm.presentation.BaseFragment
+import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity
+import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity.Companion.KEY_EDIT_STATE
+import com.boostcamp.dailyfilm.presentation.calendar.DateFragment.Companion.KEY_CALENDAR_INDEX
+import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel
+import com.boostcamp.dailyfilm.presentation.playfilm.PlayFilmActivityViewModel
+import com.boostcamp.dailyfilm.presentation.playfilm.PlayFilmBottomSheetDialog
+import com.boostcamp.dailyfilm.presentation.playfilm.PlayFilmViewModel
+import com.boostcamp.dailyfilm.presentation.playfilm.model.EditState
+import com.boostcamp.dailyfilm.presentation.selectvideo.SelectVideoActivity
+import com.boostcamp.dailyfilm.presentation.selectvideo.SelectVideoActivity.Companion.DATE_VIDEO_ITEM
+import com.boostcamp.dailyfilm.presentation.ui.theme.DailyFilmTheme
+import com.boostcamp.dailyfilm.presentation.uploadfilm.UploadFilmActivity
+import com.boostcamp.dailyfilm.presentation.uploadfilm.model.DateAndVideoModel
+import com.boostcamp.dailyfilm.presentation.util.network.NetworkManager
+import com.boostcamp.dailyfilm.presentation.util.network.NetworkState
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class PlayFilmComposeFragment :
+ BaseFragment(R.layout.fragment_play_film_compose) {
+
+ private val viewModel: PlayFilmViewModel by viewModels()
+ private val activityViewModel: PlayFilmActivityViewModel by activityViewModels()
+ private lateinit var playFilmBottomSheetDialog: PlayFilmBottomSheetDialog
+
+ private val startForResult: ActivityResultLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ if (result.resultCode == RESULT_OK && result.data != null) {
+ val text = result.data?.getStringExtra(KET_EDIT_TEXT) ?: ""
+ viewModel.setDateModel(text)
+ }
+ }
+
+ private val networkCallback = object : ConnectivityManager.NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ super.onAvailable(network)
+ viewModel.setNetworkState(NetworkState.AVAILABLE)
+ }
+
+ override fun onLost(network: Network) {
+ super.onLost(network)
+ viewModel.setNetworkState(NetworkState.LOST)
+ }
+ }
+
+ @SuppressLint("ShowToast")
+ override fun initView() {
+ binding.playFilmCompose.apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ DailyFilmTheme(requireActivity()) {
+ PlayFilmUI(
+ requireActivity(),
+ viewModel = viewModel,
+ setResultCalendar = { state ->
+ activity?.let {
+ it.setResult(
+ RESULT_OK, Intent(
+ it, CalendarActivity::class.java
+ ).apply {
+ putExtra(
+ KEY_CALENDAR_INDEX,
+ activityViewModel.calendarIndex
+ )
+ putExtra(KEY_DATE_MODEL, state.dateModel)
+ })
+ it.finish()
+ }
+ },
+ dialogEvent = { resId ->
+ when(resId) {
+ R.string.delete -> {
+ viewModel.setDialog(true)
+ }
+ R.string.re_upload -> {
+ activity?.let {
+ it.startActivity(
+ Intent(
+ it.applicationContext, SelectVideoActivity::class.java
+ ).apply {
+ putExtra(
+ KEY_CALENDAR_INDEX,
+ activityViewModel.calendarIndex
+ )
+ putExtra(KEY_DATE_MODEL, viewModel.dateModel)
+ putExtra(KEY_EDIT_STATE, EditState.RE_UPLOAD)
+ putExtra(
+ DATE_VIDEO_ITEM,
+ DateAndVideoModel(
+ viewModel.videoUri.value ?: return@PlayFilmUI,
+ viewModel.dateModel.getDate()
+ )
+ )
+ }
+ )
+ it.finish()
+ }
+ }
+ R.string.edit_text -> {
+ startForResult.launch(
+ Intent(activity?.applicationContext, UploadFilmActivity::class.java).apply {
+ putExtra(KEY_CALENDAR_INDEX, activityViewModel.calendarIndex)
+ putExtra(
+ DATE_VIDEO_ITEM,
+ DateAndVideoModel(
+ viewModel.videoUri.value ?: return@PlayFilmUI,
+ viewModel.dateModel.getDate()
+ )
+ )
+ putExtra(KEY_EDIT_STATE, EditState.EDIT_CONTENT)
+ putExtra(KEY_DATE_MODEL, viewModel.dateModel)
+ }
+ )
+ }
+ }
+ }
+ )
+ }
+ }
+ }
+ initBinding()
+ initDialog()
+ }
+
+ private fun initBinding() {
+ binding.viewModel = viewModel
+ }
+
+ private fun initDialog() {
+ playFilmBottomSheetDialog =
+ PlayFilmBottomSheetDialog(viewModel, activityViewModel, startForResult)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ NetworkManager.registerNetworkCallback(networkCallback)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ binding.backgroundPlayer.player?.play()
+ }
+
+ override fun onPause() {
+ binding.backgroundPlayer.player?.let { player ->
+ if (player.isPlaying) {
+ player.seekTo(0L)
+ player.pause()
+ }
+ }
+ super.onPause()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ NetworkManager.terminateNetworkCallback(networkCallback)
+ }
+
+ override fun onDestroyView() {
+ binding.backgroundPlayer.player?.release()
+ binding.backgroundPlayer.player = null
+ super.onDestroyView()
+ }
+
+ companion object {
+ const val KEY_DATE_MODEL = "dateModel"
+ const val KET_EDIT_TEXT = "editText"
+ fun newInstance(dateModel: DateModel) =
+ PlayFilmComposeFragment().apply {
+ arguments = Bundle().apply {
+ putParcelable(KEY_DATE_MODEL, dateModel)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/compose/PlayFilmFragmentCompose.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/compose/PlayFilmFragmentCompose.kt
new file mode 100644
index 00000000..46efa95b
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/playfilm/compose/PlayFilmFragmentCompose.kt
@@ -0,0 +1,374 @@
+package com.boostcamp.dailyfilm.presentation.playfilm.compose
+
+import android.app.Activity
+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.BoxScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+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.material.CircularProgressIndicator
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.ModalBottomSheetLayout
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.material.Text
+import androidx.compose.material.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.airbnb.lottie.LottieComposition
+import com.airbnb.lottie.compose.LottieAnimatable
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.rememberLottieAnimatable
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.boostcamp.dailyfilm.R
+import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel
+import com.boostcamp.dailyfilm.presentation.playfilm.PlayFilmViewModel
+import com.boostcamp.dailyfilm.presentation.playfilm.base.ContentShowState
+import com.boostcamp.dailyfilm.presentation.playfilm.model.BottomSheetModel
+import com.boostcamp.dailyfilm.presentation.ui.theme.blackBlur
+import com.boostcamp.dailyfilm.presentation.util.PlayState
+import com.boostcamp.dailyfilm.presentation.util.dialog.CustomDialog
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.coroutines.launch
+import me.saket.extendedspans.ExtendedSpans
+import me.saket.extendedspans.RoundedCornerSpanPainter
+import me.saket.extendedspans.SquigglyUnderlineSpanPainter
+import me.saket.extendedspans.drawBehind
+import me.saket.extendedspans.rememberSquigglyUnderlineAnimator
+import kotlin.time.Duration
+
+val playFilmBottomSheetModelList = listOf(
+ BottomSheetModel(R.drawable.ic_delete, R.string.delete),
+ BottomSheetModel(R.drawable.ic_re_upload, R.string.re_upload),
+ BottomSheetModel(R.drawable.ic_edit_text, R.string.edit_text)
+)
+
+@Composable
+fun PlayFilmUI(
+ activity: Activity,
+ viewModel: PlayFilmViewModel,
+ setResultCalendar: (PlayState.Deleted) -> Unit,
+ dialogEvent: (Int) -> Unit
+) {
+ val state = viewModel.playState.collectAsStateWithLifecycle().value
+
+ DialogUI(viewModel = viewModel)
+ PlayScreen(state, viewModel, dialogEvent)
+
+ when (state) {
+ is PlayState.Uninitialized -> {}
+ is PlayState.Loading -> {}
+ is PlayState.Playing -> {}
+ is PlayState.Deleted -> setResultCalendar(state)
+ is PlayState.Failure -> FailurePlay(activity, state)
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+private fun PlayScreen(
+ state: PlayState,
+ viewModel: PlayFilmViewModel,
+ dialogEvent: (Int) -> Unit,
+) {
+ val bottomState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
+ val scope = rememberCoroutineScope()
+
+ val muteState by viewModel.muteState.collectAsStateWithLifecycle()
+ val contentShowState by viewModel.contentShowState.collectAsStateWithLifecycle()
+ val dateModel = viewModel.dateModel
+
+ 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()
+ val textAnimatable = rememberLottieAnimatable()
+
+ val squigglyAniamtor = rememberSquigglyUnderlineAnimator(duration = Duration.parse("3s"))
+ val extendedSpans = remember {
+ ExtendedSpans(
+ RoundedCornerSpanPainter(
+ padding = RoundedCornerSpanPainter.TextPaddingValues(6.sp, 6.sp),
+ topMargin = 2.sp,
+ bottomMargin = 2.sp
+ ),
+ SquigglyUnderlineSpanPainter(wavelength = 20.sp, animator = squigglyAniamtor)
+ )
+ }
+
+ // LottieAnimation
+ LaunchedEffect(muteState.state) {
+ soundAnimatable.animate(
+ composition = soundComposition,
+ clipSpec = muteState.clipSpec,
+ )
+ }
+
+ LaunchedEffect(contentShowState.state) {
+ textAnimatable.animate(
+ composition = textComposition, clipSpec = contentShowState.clipSpec
+ )
+ }
+
+ // BottomSheetDialog
+ ModalBottomSheetLayout(
+ sheetState = bottomState,
+ sheetContent = {
+ playFilmBottomSheetModelList.forEach { model ->
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(color = colorResource(id = R.color.Background))
+ ) {
+ BottomSheetView(model, dialogEvent)
+ }
+ }
+ }) {
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Transparent)
+ .padding(dimensionResource(id = R.dimen.normal_100))
+ ) {
+ DateText(dateModel)
+
+ Row(
+ modifier = Modifier.align(Alignment.TopEnd),
+ horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.normal_100))
+ ) {
+
+ MenuImage(onClick = {
+ scope.launch {
+ bottomState.show()
+ }
+ })
+
+ SoundAnimation(
+ soundComposition = soundComposition,
+ soundAnimatable = soundAnimatable,
+ onClick = { muteState.updateState() }
+ )
+ }
+
+ ContentText(
+ extendedSpans = extendedSpans,
+ contentShowState = contentShowState,
+ dateModel = dateModel
+ )
+
+ if (state != PlayState.Playing) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+
+ TextLottieAnimation(
+ textComposition = textComposition,
+ textAnimatable = textAnimatable,
+ onClick = { contentShowState.updateState() }
+ )
+ }
+ }
+
+}
+
+@Composable
+private fun SoundAnimation(
+ soundComposition: LottieComposition?,
+ soundAnimatable: LottieAnimatable,
+ onClick: () -> Unit
+) {
+ LottieAnimation(
+ composition = soundComposition,
+ progress = soundAnimatable.progress,
+ modifier = Modifier
+ .background(blackBlur, RoundedCornerShape(4.dp))
+ .padding(4.dp)
+ .size(dimensionResource(id = R.dimen.normal_175))
+ .clickable(onClick = onClick)
+ )
+}
+
+@Composable
+private fun MenuImage(
+ onClick: () -> Unit
+) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_menu),
+ contentDescription = "Menu",
+ modifier = Modifier
+ .background(blackBlur, RoundedCornerShape(4.dp))
+ .padding(4.dp)
+ .size(dimensionResource(id = R.dimen.normal_175))
+ .clickable(onClick = onClick)
+ )
+}
+
+@Composable
+private fun BoxScope.ContentText(
+ extendedSpans: ExtendedSpans,
+ contentShowState: ContentShowState,
+ dateModel: DateModel
+) {
+ AnimatedVisibility(
+ visible = contentShowState.state,
+ modifier = Modifier.align(Alignment.Center),
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Text(
+ text = buildAnnotatedString {
+ (dateModel.text ?: "").split("\n").also { texts ->
+ texts.forEachIndexed { i, text ->
+ append(
+ extendedSpans.extend(
+ AnnotatedString(
+ text,
+ spanStyle = SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ color = MaterialTheme.colors.surface,
+ background = MaterialTheme.colors.primary
+ ),
+ )
+ )
+ )
+ if (i < texts.size) {
+ appendLine()
+ }
+ }
+ }
+ },
+ modifier = Modifier
+ .drawBehind(extendedSpans)
+ .align(Alignment.Center),
+ textAlign = TextAlign.Center,
+ color = Color.White,
+ fontSize = 16.sp
+ )
+ }
+}
+
+@Composable
+private fun BoxScope.TextLottieAnimation(
+ textComposition: LottieComposition?,
+ textAnimatable: LottieAnimatable,
+ onClick: () -> Unit
+) {
+ LottieAnimation(
+ composition = textComposition,
+ progress = textAnimatable.progress,
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .background(blackBlur, RoundedCornerShape(50.dp))
+ .size(dimensionResource(id = R.dimen.large_200))
+ .padding(4.dp)
+ .clickable(onClick = onClick)
+ )
+}
+
+@Composable
+private fun BoxScope.DateText(dateModel: DateModel) {
+ 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
+ )
+ }
+}
+
+@Preview(showBackground = true, widthDp = 320, heightDp = 80)
+@Composable
+fun BottomSheetPreView() {
+ BottomSheetView(playFilmBottomSheetModelList[0], {})
+}
+
+@Composable
+private fun BottomSheetView(model: BottomSheetModel, dialogEvent: (Int) -> Unit) {
+
+ Row(
+ modifier = Modifier
+ .padding(dimensionResource(id = R.dimen.normal_100))
+ .clickable { dialogEvent(model.title) },
+ horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.normal_125)),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ modifier = Modifier.size(dimensionResource(id = R.dimen.large_125)),
+ painter = painterResource(id = model.icon),
+ contentDescription = stringResource(id = model.title),
+ colorFilter = ColorFilter.tint(colorResource(id = R.color.OnBackground))
+ )
+ Text(
+ text = stringResource(id = model.title),
+ color = colorResource(id = R.color.OnBackground),
+ fontSize = 20.sp
+ )
+ }
+}
+
+@Composable
+private fun FailurePlay(activity: Activity, state: PlayState.Failure) {
+ state.throwable.message?.let {
+ Snackbar.make(
+ activity.findViewById(android.R.id.content), it, Snackbar.LENGTH_SHORT
+ )
+ }
+}
+
+@Composable
+private fun DialogUI(viewModel: PlayFilmViewModel) {
+ val openDialog by viewModel.openDialog.collectAsStateWithLifecycle()
+
+ // CustomDialog
+ if (openDialog) {
+ CustomDialog(
+ stringResource(id = R.string.delete_dialog),
+ { viewModel.setDialog(false) },
+ { viewModel.deleteVideo() }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/settings/SettingsViewModel.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/settings/SettingsViewModel.kt
index a2f0a96f..1f709c8c 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/settings/SettingsViewModel.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/settings/SettingsViewModel.kt
@@ -22,20 +22,17 @@ class SettingsViewModel @Inject constructor(
private val settingsRepository: SettingsRepository,
private val syncRepository: SyncRepository
) : ViewModel() {
-
private val _settingsEventFlow = MutableStateFlow(SettingsEvent.Initialized)
val settingsEventFlow: StateFlow = _settingsEventFlow.asStateFlow()
- private val _openDialog = MutableStateFlow(DialogState(false, "") {})
- val openDialog: StateFlow get() = _openDialog
-
- fun openDialog(content: String, execution: () -> Unit) {
- _openDialog.value =
- openDialog.value.copy(openDialog = true, content = content, execution = execution)
- }
+ private val _openDialog = MutableStateFlow(DialogState(false, "", {}))
+ val openDialog : StateFlow get() = _openDialog
fun closeDialog() {
- _openDialog.value = openDialog.value.copy(openDialog = false)
+ _openDialog.value = _openDialog.value.copy(openDialog = false)
+ }
+ fun openDialog(content: String, confirm: () -> Unit) {
+ _openDialog.value = _openDialog.value.copy(content = content, confirm = confirm)
}
fun backToPrevious() = event(SettingsEvent.Back)
@@ -82,7 +79,7 @@ class SettingsViewModel @Inject constructor(
data class DialogState(
val openDialog: Boolean,
val content: String,
- val execution: () -> Unit,
+ val confirm: () -> Unit,
)
sealed class SettingsEvent {
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/settings/compose/SettingComposeActivity.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/settings/compose/SettingComposeActivity.kt
index 614e0125..6f603fcd 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/settings/compose/SettingComposeActivity.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/settings/compose/SettingComposeActivity.kt
@@ -180,7 +180,7 @@ fun DialogUI(viewModel: SettingsViewModel) {
onDismiss = {
viewModel.closeDialog()
},
- confirm = openDialog.execution,
+ confirm = openDialog.confirm,
)
}
}
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/java/com/boostcamp/dailyfilm/presentation/trimvideo/TrimVideoActivity.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/trimvideo/TrimVideoActivity.kt
index f7a25804..dd6505d7 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/trimvideo/TrimVideoActivity.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/trimvideo/TrimVideoActivity.kt
@@ -19,6 +19,7 @@ import com.boostcamp.dailyfilm.presentation.calendar.model.DateModel
import com.boostcamp.dailyfilm.presentation.selectvideo.SelectVideoActivity
import com.boostcamp.dailyfilm.presentation.selectvideo.SelectVideoActivity.Companion.DATE_VIDEO_ITEM
import com.boostcamp.dailyfilm.presentation.uploadfilm.UploadFilmActivity
+import com.boostcamp.dailyfilm.presentation.uploadfilm.UploadFilmComposeActivity
import com.boostcamp.dailyfilm.presentation.uploadfilm.model.DateAndVideoModel
import com.gowtham.library.utils.CompressOption
import com.gowtham.library.utils.TrimType
@@ -100,7 +101,7 @@ class TrimVideoActivity : BaseActivity(R.layout.activi
private fun moveToUpload(trimAndVideoModel: DateAndVideoModel, startTime: Long) {
startActivity(
- Intent(this, UploadFilmActivity::class.java).apply {
+ Intent(this, UploadFilmComposeActivity::class.java).apply {
putExtra(DATE_VIDEO_ITEM, trimAndVideoModel)
putExtra(KEY_CALENDAR_INDEX, viewModel.calendarIndex)
putExtra(KEY_INFO_ITEM, viewModel.infoItem)
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/ui/theme/Color.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/ui/theme/Color.kt
index b7fa5e62..62ddebb9 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/ui/theme/Color.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/ui/theme/Color.kt
@@ -9,7 +9,7 @@ val lightBlack = Color(0xFF202022)
val lightGray = Color(0xFFE1E1E1)
val primary = lightBlack
-val primaryVariant = white
+val primaryVariant = blackBlur
val background = white
val surface = white
val error = Color(0xFFB00020)
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/ui/theme/Theme.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/ui/theme/Theme.kt
index 320793b6..2369d07f 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/ui/theme/Theme.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/ui/theme/Theme.kt
@@ -37,6 +37,7 @@ private val DarkColorPalette = darkColors(
@Composable
fun DailyFilmTheme(
+ activity: Activity = (LocalView.current.context as Activity),
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
@@ -49,7 +50,7 @@ fun DailyFilmTheme(
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
- val window = (view.context as Activity).window
+ val window = activity.window
window.statusBarColor = colors.primaryVariant.toArgb()
window.navigationBarColor = colors.primaryVariant.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme.not()
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/uploadfilm/UploadFilmCompose.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/uploadfilm/UploadFilmCompose.kt
new file mode 100644
index 00000000..f4031504
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/uploadfilm/UploadFilmCompose.kt
@@ -0,0 +1,564 @@
+package com.boostcamp.dailyfilm.presentation.uploadfilm
+
+import android.app.Activity
+import android.net.Uri
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ArrowBack
+import androidx.compose.material.icons.outlined.Check
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+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.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.airbnb.lottie.LottieComposition
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieClipSpec
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.rememberLottieAnimatable
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.boostcamp.dailyfilm.R
+import com.boostcamp.dailyfilm.presentation.playfilm.model.EditState
+import com.google.android.exoplayer2.ExoPlayer
+import com.google.android.exoplayer2.MediaItem
+import com.google.android.exoplayer2.Player
+import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
+import com.google.android.exoplayer2.ui.StyledPlayerView
+import kotlinx.coroutines.delay
+import me.saket.extendedspans.ExtendedSpans
+import me.saket.extendedspans.RoundedCornerSpanPainter
+import me.saket.extendedspans.SquigglyUnderlineSpanPainter
+import me.saket.extendedspans.drawBehind
+import me.saket.extendedspans.rememberSquigglyUnderlineAnimator
+import net.yslibrary.android.keyboardvisibilityevent.KeyboardVisibilityEvent
+import kotlin.time.Duration
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun UploadFilmScreen(
+ viewModel: UploadFilmViewModel,
+ modifier: Modifier = Modifier,
+) {
+
+ val activity = LocalContext.current as Activity
+ val editState by viewModel.editState.collectAsStateWithLifecycle()
+ val uploadUiState by viewModel.uploadUiState.collectAsStateWithLifecycle()
+ val writingState by viewModel.writingState.collectAsStateWithLifecycle()
+ val muteState by viewModel.muteState.collectAsStateWithLifecycle()
+ val compressState by viewModel.compressState.collectAsStateWithLifecycle()
+ val snackbarHostState = remember { SnackbarHostState() }
+ val focusRequester = remember { FocusRequester() }
+
+ LaunchedEffect(Unit) {
+ KeyboardVisibilityEvent.setEventListener(activity) { viewModel.updateIsWriting(it) }
+ }
+
+ LaunchedEffect(uploadUiState) {
+ when (uploadUiState) {
+ is UploadUiState.UploadFailed -> {
+ val state = uploadUiState as UploadUiState.UploadFailed
+ state.throwable.message?.let {
+ snackbarHostState.showSnackbar(it)
+ }
+ }
+
+ else -> {}
+ }
+ }
+
+ when (writingState) {
+ true -> focusRequester.requestFocus()
+ false -> LocalFocusManager.current.clearFocus()
+ }
+
+ BackgroundVideoPlayer(
+ originUri = viewModel.beforeItem?.uri,
+ resultUri = viewModel.infoItem?.uri,
+ startTime = viewModel.startTime,
+ editState = editState,
+ muteState = muteState,
+ modifier = Modifier.fillMaxSize()
+ )
+
+ Scaffold(
+ snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
+ containerColor = Color.Transparent,
+ topBar = {
+ UploadFilmTopArea(
+ writingState = writingState,
+ muteState = muteState,
+ backAction = viewModel::cancelUploadVideo,
+ uploadAction = viewModel::uploadVideo,
+ muteAction = viewModel::controlSound,
+ editTextAction = viewModel::changeIsWriting,
+ modifier = Modifier
+ .background(color = Color.Transparent)
+ .fillMaxWidth()
+ .padding(4.dp)
+ )
+ },
+ content = { innerPadding ->
+ UploadFilmMainArea(
+ compressVal = compressState,
+ focusRequester = focusRequester,
+ onTextChanged = viewModel::updateTextContent,
+ onKeyboardHide = viewModel::changeIsWriting,
+ modifier = Modifier
+ .padding(innerPadding)
+ .fillMaxSize()
+ .background(color = Color.Transparent)
+ )
+ },
+ modifier = modifier
+ .background(color = Color.Transparent)
+ )
+
+ // 업로드 로딩 Composition
+ if (uploadUiState is UploadUiState.UploadLoading)
+ UploadLoadingProgress(
+ modifier = Modifier
+ .fillMaxSize()
+ )
+
+}
+
+@Composable
+fun UploadFilmTopArea(
+ writingState: Boolean,
+ muteState: Boolean,
+ backAction: () -> Unit,
+ uploadAction: () -> Unit,
+ muteAction: () -> Unit,
+ editTextAction: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+
+ val muteComposition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.lottie_sound))
+ val writingComposition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.lottie_writing))
+
+ Box(modifier = modifier) {
+ val muteAnimatable = rememberLottieAnimatable()
+ LaunchedEffect(muteState) {
+ muteAnimatable.animate(
+ muteComposition,
+ clipSpec = when (muteState) {
+ true -> LottieClipSpec.Progress(0f, 0.5f)
+ false -> LottieClipSpec.Progress(0.5f, 1.0f)
+ },
+ speed = 5f
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .align(Alignment.CenterStart)
+ .wrapContentSize()
+ .background(color = Color.Transparent)
+ ) {
+
+ // 뒤로 가기 버튼
+ TopButton(
+ onClick = backAction,
+ icon = Icons.Outlined.ArrowBack
+ )
+
+ LottieButton(
+ composition = muteComposition,
+ progress = muteAnimatable.progress,
+ onClick = muteAction,
+ modifier = Modifier
+ .padding(start = 12.dp, top = 4.dp, bottom = 4.dp)
+ )
+ }
+
+ val writingAnimatable = rememberLottieAnimatable()
+ LaunchedEffect(writingState) {
+ writingAnimatable.animate(
+ writingComposition,
+ clipSpec = when (writingState) {
+ true -> LottieClipSpec.Progress(0f, 0.5f)
+ false -> LottieClipSpec.Progress(0.5f, 1f)
+ },
+ speed = 2f
+ )
+ }
+ LottieButton(
+ composition = writingComposition,
+ progress = writingAnimatable.progress,
+ onClick = editTextAction,
+ modifier = Modifier
+ .align(Alignment.Center)
+ )
+
+ // 업로드 버튼
+ TopButton(
+ onClick = uploadAction,
+ icon = Icons.Outlined.Check,
+ modifier = Modifier.align(Alignment.CenterEnd)
+ )
+
+ }
+
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun UploadFilmMainArea(
+ compressVal: Int,
+ focusRequester: FocusRequester,
+ onTextChanged: (String) -> Unit,
+ onKeyboardHide: () -> Unit,
+ modifier: Modifier
+) {
+
+ Box(modifier = modifier) {
+
+ LinearProgressIndicator(
+ progress = (compressVal.toFloat() / 240f),
+ color = MaterialTheme.colors.primary,
+ trackColor = MaterialTheme.colors.surface,
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.TopCenter)
+ )
+
+ val textContent = remember { mutableStateOf("") }
+ val squigglyAniamtor = rememberSquigglyUnderlineAnimator(duration = Duration.parse("3s"))
+ val extendedSpans = remember {
+ ExtendedSpans(
+ RoundedCornerSpanPainter(
+ padding = RoundedCornerSpanPainter.TextPaddingValues(6.sp, 6.sp),
+ topMargin = 2.sp,
+ bottomMargin = 2.sp
+ ),
+ SquigglyUnderlineSpanPainter(wavelength = 20.sp, animator = squigglyAniamtor)
+ )
+ }
+
+ // 실제로 입력받는 필드 (보이지는 않음)
+ TextField(
+ value = textContent.value,
+ onValueChange = { textValue ->
+ textContent.value = textValue
+ onTextChanged(textValue)
+ },
+ textStyle = TextStyle(
+ color = Color.Transparent, // 실제로는 보여주지 않기
+ fontSize = 22.sp,
+ textAlign = TextAlign.Center
+ ),
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Default),
+ keyboardActions = KeyboardActions(onDone = { onKeyboardHide() }),
+ colors = TextFieldDefaults.textFieldColors(
+ containerColor = Color.Transparent,
+ placeholderColor = Color.Transparent,
+ cursorColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent,
+ disabledIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ ),
+ modifier = Modifier
+ .align(Alignment.Center)
+ .background(color = Color.Transparent)
+ .focusable(true)
+ .focusRequester(focusRequester)
+ )
+
+ // 눈에 보이는 텍스트
+ Text(
+ modifier = Modifier
+ .drawBehind(extendedSpans)
+ .align(Alignment.Center),
+ text =
+ buildAnnotatedString {
+ textContent.value.split("\n").also { texts ->
+ texts.forEachIndexed { i, text ->
+ append(
+ extendedSpans.extend(
+ AnnotatedString(
+ text,
+ spanStyle = SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ color = MaterialTheme.colors.surface,
+ background = MaterialTheme.colors.primary
+ ),
+ )
+ )
+ )
+
+ if (i < texts.size)
+ appendLine()
+ }
+ }
+ },
+ fontSize = 22.sp,
+ textAlign = TextAlign.Center,
+ onTextLayout = { result ->
+ extendedSpans.onTextLayout(result)
+ }
+ )
+
+ }
+
+}
+
+@Composable
+fun TopButton(
+ onClick: () -> Unit,
+ icon: ImageVector,
+ modifier: Modifier = Modifier
+) {
+ FilledIconButton(
+ onClick = onClick,
+ colors = IconButtonDefaults.filledIconButtonColors(
+ containerColor = MaterialTheme.colors.primaryVariant,
+ contentColor = MaterialTheme.colors.surface
+ ),
+ modifier = modifier
+ ) {
+ Icon(icon, contentDescription = null)
+ }
+}
+
+@Composable
+fun BackgroundVideoPlayer(
+ originUri: Uri?,
+ resultUri: Uri?,
+ startTime: Long,
+ editState: EditState?,
+ muteState: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ if (editState == null) return
+
+ val context = LocalContext.current
+ val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
+ val mediaItem = remember {
+ var media = MediaItem.EMPTY
+ when (editState) {
+ EditState.EDIT_CONTENT -> {
+ if (resultUri != null) media = MediaItem.fromUri(resultUri)
+ }
+
+ EditState.NEW_UPLOAD, EditState.RE_UPLOAD -> {
+ if (originUri != null) {
+ media = MediaItem.fromUri(originUri)
+ }
+ }
+ }
+ media
+ }
+
+ val exoPlayer = remember {
+ ExoPlayer.Builder(context).build().apply {
+ volume = 0.5f
+ repeatMode = Player.REPEAT_MODE_ONE
+ setMediaItem(mediaItem)
+ prepare()
+ play()
+ }
+ }
+
+ LaunchedEffect(mediaItem) {
+ if (editState == EditState.NEW_UPLOAD || editState == EditState.RE_UPLOAD) {
+ if (originUri != null) {
+ while (true) {
+ exoPlayer.seekTo(startTime)
+ delay(10_000)
+ }
+ }
+ }
+ }
+
+ LaunchedEffect(muteState) {
+ when (muteState) {
+ true -> exoPlayer.volume = 0.0f
+ false -> exoPlayer.volume = 0.5f
+ }
+ }
+
+ DisposableEffect(
+ AndroidView(
+ modifier = modifier,
+ factory = {
+ StyledPlayerView(it).apply {
+ player = exoPlayer
+ useController = false
+ layoutParams =
+ FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
+ }
+ })
+ ) {
+ lifecycleOwner.value.lifecycle.addObserver(
+ LifecycleEventObserver { _, event ->
+ when(event) {
+ Lifecycle.Event.ON_PAUSE -> exoPlayer.pause()
+ Lifecycle.Event.ON_RESUME -> exoPlayer.play()
+ else -> {}
+ }
+ }
+ )
+
+ onDispose { exoPlayer.release() }
+ }
+
+
+}
+
+@Composable
+fun LottieButton(
+ composition: LottieComposition?,
+ progress: Float,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+
+ Box(modifier = modifier
+ .background(
+ color = MaterialTheme.colors.primaryVariant,
+ shape = CircleShape
+ )
+ .size(40.dp)
+ .clip(CircleShape)
+ .clickable { onClick() }
+ ) {
+ LottieAnimation(
+ composition = composition,
+ progress = { progress },
+ modifier = Modifier
+ .padding(8.dp)
+ .align(Alignment.Center)
+ .fillMaxSize()
+ )
+ }
+
+}
+
+@Composable
+fun UploadLoadingProgress(
+ modifier: Modifier = Modifier
+) {
+
+ Column(
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = modifier
+ .background(MaterialTheme.colors.primaryVariant)
+ ) {
+
+ CircularProgressIndicator(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .size(100.dp),
+ color = MaterialTheme.colors.surface,
+ )
+
+ }
+
+}
+
+@Preview
+@Composable
+fun PreviewLottieButton() {
+ LottieButton(
+ composition = LottieComposition(),
+ progress = 0f,
+ onClick = {}
+ )
+}
+
+@Preview
+@Composable
+fun PreviewLoadingProgress() {
+ UploadLoadingProgress()
+}
+
+@Preview
+@Composable
+fun PreviewUploadFilmTopArea() {
+ UploadFilmTopArea(
+ writingState = true,
+ muteState = true,
+ backAction = {},
+ uploadAction = {},
+ muteAction = {},
+ editTextAction = {},
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ )
+}
+
+@Preview
+@Composable
+fun PreviewUploadFilmMainArea() {
+ UploadFilmMainArea(
+ compressVal = 240,
+ focusRequester = FocusRequester(),
+ onTextChanged = {},
+ onKeyboardHide = {},
+ modifier = Modifier
+ .fillMaxSize()
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/uploadfilm/UploadFilmComposeActivity.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/uploadfilm/UploadFilmComposeActivity.kt
new file mode 100644
index 00000000..830ea83d
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/uploadfilm/UploadFilmComposeActivity.kt
@@ -0,0 +1,106 @@
+package com.boostcamp.dailyfilm.presentation.uploadfilm
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity
+import com.boostcamp.dailyfilm.presentation.calendar.DateFragment
+import com.boostcamp.dailyfilm.presentation.playfilm.PlayFilmActivity
+import com.boostcamp.dailyfilm.presentation.playfilm.PlayFilmFragment
+import com.boostcamp.dailyfilm.presentation.playfilm.model.EditState
+import com.boostcamp.dailyfilm.presentation.selectvideo.SelectVideoActivity
+import com.boostcamp.dailyfilm.presentation.trimvideo.TrimVideoActivity
+import com.boostcamp.dailyfilm.presentation.ui.theme.DailyFilmTheme
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+import java.io.File
+
+@AndroidEntryPoint
+class UploadFilmComposeActivity : ComponentActivity() {
+
+ private val viewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ subscribeUiState()
+
+ setContent {
+ DailyFilmTheme {
+ UploadFilmScreen(viewModel = viewModel)
+ }
+ }
+ }
+
+ private fun subscribeUiState() {
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.uploadUiState.collect { uiState ->
+ when (uiState) {
+ is UploadUiState.Canceled -> {
+ if (viewModel.beforeItem != null) {
+ // 업로드 안 한 영상은 로컬에서도 삭제
+ deleteLocalFile(viewModel.infoItem?.uri?.path)
+
+ // 돌아가기
+ startActivity(
+ Intent(
+ this@UploadFilmComposeActivity,
+ TrimVideoActivity::class.java
+ ).apply {
+ putExtra(CalendarActivity.KEY_EDIT_STATE, viewModel.editState.value)
+ putExtra(SelectVideoActivity.DATE_VIDEO_ITEM, viewModel.beforeItem)
+ putExtra(PlayFilmFragment.KEY_DATE_MODEL, viewModel.dateModel)
+ putExtra(DateFragment.KEY_CALENDAR_INDEX, viewModel.calendarIndex)
+ }
+ )
+ }
+ finish()
+ }
+
+ is UploadUiState.UploadSuccess -> {
+ when (viewModel.editState.value) {
+ EditState.EDIT_CONTENT -> {
+ setResult(
+ Activity.RESULT_OK,
+ Intent(
+ this@UploadFilmComposeActivity,
+ PlayFilmActivity::class.java
+ ).apply {
+ putExtra(PlayFilmFragment.KET_EDIT_TEXT, uiState.dateModel.text)
+ }
+ )
+ }
+ else -> {
+ setResult(
+ Activity.RESULT_OK,
+ Intent(
+ this@UploadFilmComposeActivity,
+ CalendarActivity::class.java
+ ).apply {
+ putExtra(DateFragment.KEY_CALENDAR_INDEX, viewModel.calendarIndex)
+ putExtra(PlayFilmFragment.KEY_DATE_MODEL, uiState.dateModel)
+ }
+ )
+ }
+ }
+ finish()
+ }
+
+ else -> {}
+ }
+ }
+ }
+ }
+ }
+
+ private fun deleteLocalFile(filePath: String?) {
+ filePath?.let { File(it).delete() }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/uploadfilm/UploadFilmViewModel.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/uploadfilm/UploadFilmViewModel.kt
index 88b3b21e..1ffb3525 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/uploadfilm/UploadFilmViewModel.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/uploadfilm/UploadFilmViewModel.kt
@@ -24,6 +24,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -39,7 +40,7 @@ class UploadFilmViewModel @Inject constructor(
val startTime = savedStateHandle.get(KEY_START_TIME) ?: 0L
val dateModel = savedStateHandle.get(KEY_DATE_MODEL)
val calendarIndex = savedStateHandle.get(KEY_CALENDAR_INDEX)
- val editState = savedStateHandle.get(KEY_EDIT_STATE)
+ val editState = savedStateHandle.getStateFlow(KEY_EDIT_STATE, null)
private val _uploadResult = MutableSharedFlow()
val uploadResult: SharedFlow get() = _uploadResult
@@ -61,12 +62,25 @@ class UploadFilmViewModel @Inject constructor(
private val _isWriting = MutableLiveData(false)
val isWriting: LiveData get() = _isWriting
- private val _clickSound = MutableStateFlow(true)
- val clickSound = _clickSound.asStateFlow()
-
private val _compressProgress = MutableLiveData(0)
val compressProgress: LiveData get() = _compressProgress
+
+ private val _uploadUiState = MutableStateFlow(UploadUiState.Idle)
+ val uploadUiState: StateFlow get() = _uploadUiState
+
+ private val _writingState = MutableStateFlow(false)
+ val writingState: StateFlow get() = _writingState
+
+ private val _muteState = MutableStateFlow(false)
+ val muteState = _muteState.asStateFlow()
+
+ private val _contentState = MutableStateFlow("")
+
+ private val _compressState = MutableStateFlow(0)
+ val compressState: StateFlow get() = _compressState
+
+
init {
calcProgress()
}
@@ -75,10 +89,10 @@ class UploadFilmViewModel @Inject constructor(
Config.resetStatistics()
Config.enableStatisticsCallback {
val percentage = it.videoFrameNumber
- _compressProgress.postValue(percentage)
+ _compressState.value = percentage
if (isEnded())
- _compressProgress.postValue(240)
+ _compressState.value = 240
}
}
@@ -90,21 +104,22 @@ class UploadFilmViewModel @Inject constructor(
fun uploadVideo() {
- val text = textContent.value ?: ""
- val progress = _compressProgress.value ?: 0
+ val text = _contentState.value ?: ""
+ val progress = _compressState.value
when {
text.isEmpty() -> {
- _uiState.value = UiState.Failure(Throwable("영상에 맞는 문구를 입력해주세요."))
+ _uploadUiState.value = UploadUiState.UploadFailed(Throwable("일기가 비어있습니다"))
return
}
- (editState != EditState.EDIT_CONTENT) && progress < 240 -> {
- _uiState.value = UiState.Failure(Throwable("영상 처리중입니다. 잠시만 기다려주세요."))
+ (editState.value != EditState.EDIT_CONTENT) && progress < 240 -> {
+ _uploadUiState.value = UploadUiState.UploadFailed(Throwable("영상이 처리중입니다. 잠시만 기다려주세요"))
return
}
}
- editState?.let { state ->
+ _uploadUiState.value = UploadUiState.UploadLoading
+ editState.value?.let { state ->
when (state) {
EditState.NEW_UPLOAD -> uploadStorage()
EditState.EDIT_CONTENT -> uploadEdit()
@@ -115,22 +130,19 @@ class UploadFilmViewModel @Inject constructor(
private fun uploadEdit() {
- val text = textContent.value ?: ""
+ val text = _contentState.value ?: ""
infoItem?.let { item ->
- _uiState.value = UiState.Loading
viewModelScope.launch {
dateModel ?: return@launch
val date = item.uploadDate
val dailyFilmItem = DailyFilmItem(dateModel.videoUrl.toString(), text, date)
when (val result = uploadFilmRepository.uploadEditVideo(date, dailyFilmItem)) {
is Result.Success -> {
- _uiState.value = UiState.Success(
- dateModel.copy(text = text)
- )
+ _uploadUiState.value = UploadUiState.UploadSuccess(dateModel.copy(text = text))
}
is Result.Error -> {
- _uiState.value = UiState.Failure(result.exception)
+ _uploadUiState.value = UploadUiState.UploadFailed(result.exception)
}
}
}
@@ -139,14 +151,13 @@ class UploadFilmViewModel @Inject constructor(
private fun uploadStorage() {
infoItem?.let { item ->
- _uiState.value = UiState.Loading
viewModelScope.launch {
when (val result = uploadFilmRepository.uploadVideo(item.uploadDate, item.uri)) {
is Result.Success -> {
uploadRealtime(result.data)
}
is Result.Error -> {
- _uiState.value = UiState.Failure(result.exception)
+ _uploadUiState.value = UploadUiState.UploadFailed(result.exception)
}
}
}
@@ -155,9 +166,9 @@ class UploadFilmViewModel @Inject constructor(
private fun uploadRealtime(videoUrl: Uri?) {
val uploadDate = infoItem?.uploadDate
- val text = textContent.value ?: ""
+ val text = _contentState.value ?: ""
if (dateModel ==null){
- _uiState.value = UiState.Failure(Throwable("dateModel Fail"))
+ _uploadUiState.value = UploadUiState.UploadFailed(Throwable("dateModel failed"))
return
}
if (videoUrl != null && uploadDate != null) {
@@ -166,23 +177,23 @@ class UploadFilmViewModel @Inject constructor(
when (val result = uploadFilmRepository.uploadFilmInfo(uploadDate, filmItem)) {
is Result.Success -> {
uploadFilmRepository.insertFilmEntity(filmItem)
- _uiState.value = UiState.Success(
+ _uploadUiState.value = UploadUiState.UploadSuccess(
dateModel.copy(text = text, videoUrl = videoUrl.toString())
)
}
is Result.Error -> {
- _uiState.value = UiState.Failure(result.exception)
+ _uploadUiState.value = UploadUiState.UploadFailed(result.exception)
}
}
}
} else {
- _uiState.value =
- UiState.Failure(Throwable("userId == null or videoUrl == null or uploadDate or null "))
+ _uploadUiState.value =
+ UploadUiState.UploadFailed(Throwable("userId or videoUrl or uploadDate is null"))
}
}
fun updateSpannableText() {
- textContent.value?.let { text ->
+ _contentState.value.let { text ->
if (text.isNotEmpty()) {
_showedTextContent.value = SpannableString(text).apply {
setSpan(
@@ -198,11 +209,14 @@ class UploadFilmViewModel @Inject constructor(
}
}
+ fun updateTextContent(text: String) {
+ _contentState.value = text
+ }
+
private fun deleteVideo() {
dateModel ?: return
viewModelScope.launch {
- _uiState.value = UiState.Loading
val updateDate = dateModel.getDate()
when (val result = deleteFilmRepository.delete(updateDate)) {
@@ -210,29 +224,27 @@ class UploadFilmViewModel @Inject constructor(
uploadStorage()
}
is Result.Error -> {
- UiState.Failure(result.exception)
+ _uploadUiState.value = UploadUiState.UploadFailed(result.exception)
}
}
}
}
fun changeIsWriting() {
- _isWriting.value?.let {
- _isWriting.value = it.not()
- }
+ _writingState.value = _writingState.value.not()
}
fun updateIsWriting(flag: Boolean) {
- _isWriting.value = flag
+ _writingState.value = flag
}
fun controlSound() {
- _clickSound.value = !_clickSound.value
+ _muteState.value = !_muteState.value
}
fun cancelUploadVideo() {
viewModelScope.launch {
- _cancelUploadResult.emit(true)
+ _uploadUiState.emit(UploadUiState.Canceled)
}
}
@@ -241,3 +253,16 @@ class UploadFilmViewModel @Inject constructor(
const val KEY_START_TIME = "start_time"
}
}
+
+sealed interface UploadUiState {
+
+ object Idle: UploadUiState
+
+ object Canceled: UploadUiState
+ object UploadLoading: UploadUiState
+
+ data class UploadSuccess(val dateModel: DateModel): UploadUiState
+
+ data class UploadFailed(val throwable: Throwable): UploadUiState
+
+}
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/util/dialog/CustomDialog.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/util/dialog/CustomDialog.kt
index 1900eec2..15309d5f 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/util/dialog/CustomDialog.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/util/dialog/CustomDialog.kt
@@ -13,7 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -29,14 +29,14 @@ fun CustomDialog(text: String, onDismiss: () -> Unit, confirm: () -> Unit) {
.fillMaxWidth()
.wrapContentHeight()
.clip(RoundedCornerShape(12.dp))
- .background(color = Color.White),
+ .background(color = colorResource(id = R.color.Background)),
) {
Text(
modifier = Modifier
.padding(top = 24.dp)
.padding(horizontal = 24.dp),
text = text,
- color = Color.Black,
+ color = colorResource(id = R.color.OnBackground),
)
Row(
modifier = Modifier
@@ -49,13 +49,13 @@ fun CustomDialog(text: String, onDismiss: () -> Unit, confirm: () -> Unit) {
modifier = Modifier
.padding(end = 20.dp)
.clickable(onClick = onDismiss),
- color = Color.Black,
+ color = colorResource(id = R.color.OnBackground),
)
Text(
text = stringResource(id = R.string.confirm),
modifier = Modifier
.clickable(onClick = confirm),
- color = Color.Black,
+ color = colorResource(id = R.color.OnBackground),
)
}
}
diff --git a/app/src/main/res/layout/fragment_play_film.xml b/app/src/main/res/layout/fragment_play_film.xml
index cbdeee9a..0d2adf81 100644
--- a/app/src/main/res/layout/fragment_play_film.xml
+++ b/app/src/main/res/layout/fragment_play_film.xml
@@ -39,7 +39,7 @@
android:id="@+id/backgroundPlayer"
android:layout_width="0dp"
android:layout_height="0dp"
- app:changeVolume="@{viewModel.isMuted()}"
+ app:changeVolume="@{viewModel.muteState.state}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -78,7 +78,7 @@
android:layout_marginEnd="@dimen/normal_100"
android:alpha="0.5"
android:background="@drawable/background_rounded"
- android:onClick="@{() -> viewModel.changeMuteState()}"
+ android:onClick="@{() -> viewModel.muteState.updateState()}"
android:padding="@dimen/small_100"
android:scaleType="fitStart"
app:layout_constraintBottom_toBottomOf="@+id/tv_date"
@@ -87,7 +87,7 @@
app:lottie_autoPlay="false"
app:lottie_fileName="sound_lottie.json"
app:lottie_loop="false"
- app:syncMuteIcon="@{viewModel.isMuted()}" />
+ app:syncMuteIcon="@{viewModel.muteState.state}" />
+ app:visibilityAnimation="@{viewModel.contentShowState.state}" />
+ app:syncViewState="@{viewModel.contentShowState.state}" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_play_film_compose.xml b/app/src/main/res/layout/fragment_play_film_compose.xml
new file mode 100644
index 00000000..b8ed3e57
--- /dev/null
+++ b/app/src/main/res/layout/fragment_play_film_compose.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/raw/lottie_sound.json b/app/src/main/res/raw/lottie_sound.json
new file mode 100644
index 00000000..069506ac
--- /dev/null
+++ b/app/src/main/res/raw/lottie_sound.json
@@ -0,0 +1,3464 @@
+{
+ "v": "5.1.18",
+ "fr": 29.9700012207031,
+ "ip": 0,
+ "op": 90.0000036657751,
+ "w": 20,
+ "h": 20,
+ "nm": "Mute",
+ "ddd": 0,
+ "assets": [],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "mask_wave_2",
+ "td": 1,
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 10,
+ 10,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "rc",
+ "d": 1,
+ "s": {
+ "a": 0,
+ "k": [
+ 9.938,
+ 18.313
+ ],
+ "ix": 2
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "nm": "Rectangle Path 1",
+ "mn": "ADBE Vector Shape - Rect",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ -5.281,
+ 0.031
+ ],
+ "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": "Rectangle 1",
+ "np": 3,
+ "cix": 2,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 91.000003706506,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 2,
+ "ty": 4,
+ "nm": "wave_2 Outlines",
+ "tt": 2,
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.833
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.167
+ },
+ "n": "0p833_0p833_0p167_0p167",
+ "t": 2,
+ "s": [
+ 18.306,
+ 9.963,
+ 0
+ ],
+ "e": [
+ 18.118,
+ 9.963,
+ 0
+ ],
+ "to": [
+ -0.03125,
+ 0,
+ 0
+ ],
+ "ti": [
+ 1.67708337306976,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.931
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.167
+ },
+ "n": "0p833_0p931_0p167_0p167",
+ "t": 6,
+ "s": [
+ 18.118,
+ 9.963,
+ 0
+ ],
+ "e": [
+ 8.243,
+ 9.963,
+ 0
+ ],
+ "to": [
+ -1.67708337306976,
+ 0,
+ 0
+ ],
+ "ti": [
+ 1.64583337306976,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.833
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.167
+ },
+ "n": "0p833_0p833_0p167_0p167",
+ "t": 11,
+ "s": [
+ 8.243,
+ 9.963,
+ 0
+ ],
+ "e": [
+ 8.243,
+ 9.963,
+ 0
+ ],
+ "to": [
+ 0,
+ 0,
+ 0
+ ],
+ "ti": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.833
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.069
+ },
+ "n": "0p833_0p833_0p167_0p069",
+ "t": 75,
+ "s": [
+ 8.243,
+ 9.963,
+ 0
+ ],
+ "e": [
+ 18.118,
+ 9.963,
+ 0
+ ],
+ "to": [
+ 1.64583337306976,
+ 0,
+ 0
+ ],
+ "ti": [
+ -1.67708337306976,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.833
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.167
+ },
+ "n": "0p833_0p833_0p167_0p167",
+ "t": 80,
+ "s": [
+ 18.118,
+ 9.963,
+ 0
+ ],
+ "e": [
+ 18.306,
+ 9.963,
+ 0
+ ],
+ "to": [
+ 1.67708337306976,
+ 0,
+ 0
+ ],
+ "ti": [
+ -0.03125,
+ 0,
+ 0
+ ]
+ },
+ {
+ "t": 84.0000034213901
+ }
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 2.827,
+ 7.401,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833,
+ 0.833,
+ 0.833
+ ],
+ "y": [
+ 0.931,
+ 0.931,
+ 1.151
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167,
+ 0.167,
+ 0.167
+ ],
+ "y": [
+ 0.069,
+ 0.069,
+ -0.151
+ ]
+ },
+ "n": [
+ "0p833_0p931_0p167_0p069",
+ "0p833_0p931_0p167_0p069",
+ "0p833_1p151_0p167_-0p151"
+ ],
+ "t": 6,
+ "s": [
+ 100,
+ 100,
+ 100
+ ],
+ "e": [
+ 44.871,
+ 44.871,
+ 100
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833,
+ 0.833,
+ 0.833
+ ],
+ "y": [
+ 1,
+ 1,
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167,
+ 0.167,
+ 0.167
+ ],
+ "y": [
+ -49.003,
+ -49.003,
+ -1.938
+ ]
+ },
+ "n": [
+ "0p833_1_0p167_-49p003",
+ "0p833_1_0p167_-49p003",
+ "0p833_1_0p167_-1p938"
+ ],
+ "t": 11,
+ "s": [
+ 44.871,
+ 44.871,
+ 100
+ ],
+ "e": [
+ 44.871,
+ 44.871,
+ 100
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833,
+ 0.833,
+ 0.833
+ ],
+ "y": [
+ 1,
+ 1,
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167,
+ 0.167,
+ 0.167
+ ],
+ "y": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ "n": [
+ "0p833_1_0p167_0",
+ "0p833_1_0p167_0",
+ "0p833_1_0p167_0"
+ ],
+ "t": 75,
+ "s": [
+ 44.871,
+ 44.871,
+ 100
+ ],
+ "e": [
+ 100,
+ 100,
+ 100
+ ]
+ },
+ {
+ "t": 80.0000032584668
+ }
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0.192,
+ 0
+ ],
+ [
+ 0.146,
+ 0.147
+ ],
+ [
+ -0.293,
+ 0.293
+ ],
+ [
+ 3.217,
+ 3.217
+ ],
+ [
+ -0.293,
+ 0.293
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 3.801,
+ -3.801
+ ]
+ ],
+ "o": [
+ [
+ -0.192,
+ 0
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 3.217,
+ -3.217
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0.293,
+ -0.293
+ ],
+ [
+ 3.801,
+ 3.802
+ ],
+ [
+ -0.146,
+ 0.147
+ ]
+ ],
+ "v": [
+ [
+ -1.406,
+ 7.151
+ ],
+ [
+ -1.936,
+ 6.931
+ ],
+ [
+ -1.936,
+ 5.871
+ ],
+ [
+ -1.936,
+ -5.797
+ ],
+ [
+ -1.936,
+ -6.858
+ ],
+ [
+ -0.876,
+ -6.858
+ ],
+ [
+ -0.876,
+ 6.931
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "mm",
+ "mm": 4,
+ "nm": "Merge Paths 1",
+ "mn": "ADBE Vector Filter - Merge",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 2.479,
+ 7.401
+ ],
+ "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": 3,
+ "cix": 2,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 91.000003706506,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 3,
+ "ty": 4,
+ "nm": "mask_wave",
+ "td": 1,
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 10,
+ 10,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "rc",
+ "d": 1,
+ "s": {
+ "a": 0,
+ "k": [
+ 9.938,
+ 18.313
+ ],
+ "ix": 2
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "nm": "Rectangle Path 1",
+ "mn": "ADBE Vector Shape - Rect",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ -5.281,
+ 0.031
+ ],
+ "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": "Rectangle 1",
+ "np": 3,
+ "cix": 2,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 91.000003706506,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 4,
+ "ty": 4,
+ "nm": "wave_1 Outlines",
+ "tt": 2,
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.975
+ },
+ "o": {
+ "x": 0.333,
+ "y": 0.052
+ },
+ "n": "0p833_0p975_0p333_0p052",
+ "t": 0,
+ "s": [
+ 14.063,
+ 9.959,
+ 0
+ ],
+ "e": [
+ 14.813,
+ 9.959,
+ 0
+ ],
+ "to": [
+ 0.125,
+ 0,
+ 0
+ ],
+ "ti": [
+ 1.52083337306976,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.667,
+ "y": 1
+ },
+ "o": {
+ "x": 0.333,
+ "y": 0
+ },
+ "n": "0p667_1_0p333_0",
+ "t": 4,
+ "s": [
+ 14.813,
+ 9.959,
+ 0
+ ],
+ "e": [
+ 4.938,
+ 9.959,
+ 0
+ ],
+ "to": [
+ -1.52083337306976,
+ 0,
+ 0
+ ],
+ "ti": [
+ 1.64583337306976,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.667,
+ "y": 0.667
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.167
+ },
+ "n": "0p667_0p667_0p167_0p167",
+ "t": 11,
+ "s": [
+ 4.938,
+ 9.959,
+ 0
+ ],
+ "e": [
+ 4.938,
+ 9.959,
+ 0
+ ],
+ "to": [
+ 0,
+ 0,
+ 0
+ ],
+ "ti": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.992
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.008
+ },
+ "n": "0p833_0p992_0p167_0p008",
+ "t": 75,
+ "s": [
+ 4.938,
+ 9.959,
+ 0
+ ],
+ "e": [
+ 14.813,
+ 9.959,
+ 0
+ ],
+ "to": [
+ 1.64583337306976,
+ 0,
+ 0
+ ],
+ "ti": [
+ -1.52083337306976,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.833
+ },
+ "o": {
+ "x": 0.333,
+ "y": 0
+ },
+ "n": "0p833_0p833_0p333_0",
+ "t": 82,
+ "s": [
+ 14.813,
+ 9.959,
+ 0
+ ],
+ "e": [
+ 14.063,
+ 9.959,
+ 0
+ ],
+ "to": [
+ 1.52083337306976,
+ 0,
+ 0
+ ],
+ "ti": [
+ 0.125,
+ 0,
+ 0
+ ]
+ },
+ {
+ "t": 86.0000035028518
+ }
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 1.95,
+ 4.218,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0.192,
+ 0
+ ],
+ [
+ 0.146,
+ 0.147
+ ],
+ [
+ -0.293,
+ 0.293
+ ],
+ [
+ 1.462,
+ 1.462
+ ],
+ [
+ -0.293,
+ 0.293
+ ],
+ [
+ -0.293,
+ -0.292
+ ],
+ [
+ 2.046,
+ -2.048
+ ]
+ ],
+ "o": [
+ [
+ -0.191,
+ 0
+ ],
+ [
+ -0.293,
+ -0.292
+ ],
+ [
+ 1.462,
+ -1.462
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0.293,
+ -0.292
+ ],
+ [
+ 2.046,
+ 2.047
+ ],
+ [
+ -0.146,
+ 0.147
+ ]
+ ],
+ "v": [
+ [
+ -0.876,
+ 3.969
+ ],
+ [
+ -1.406,
+ 3.749
+ ],
+ [
+ -1.406,
+ 2.689
+ ],
+ [
+ -1.406,
+ -2.615
+ ],
+ [
+ -1.406,
+ -3.676
+ ],
+ [
+ -0.346,
+ -3.676
+ ],
+ [
+ -0.346,
+ 3.749
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 1.95,
+ 4.218
+ ],
+ "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": 2,
+ "cix": 2,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 91.000003706506,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 5,
+ "ty": 4,
+ "nm": "bar Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 10.5,
+ 9.463,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 6.475,
+ 6.439,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": 0.667,
+ "y": 1
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.167
+ },
+ "n": "0p667_1_0p167_0p167",
+ "t": 19,
+ "s": [
+ {
+ "i": [
+ [
+ 0.192,
+ 0
+ ],
+ [
+ 0.146,
+ 0.147
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.293,
+ 0.293
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.293,
+ -0.293
+ ]
+ ],
+ "o": [
+ [
+ -0.192,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0.293,
+ -0.293
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.293,
+ 0.293
+ ],
+ [
+ -0.146,
+ 0.147
+ ]
+ ],
+ "v": [
+ [
+ -5.569,
+ -4.777
+ ],
+ [
+ -6.099,
+ -4.996
+ ],
+ [
+ -5.932,
+ -4.836
+ ],
+ [
+ -5.932,
+ -5.896
+ ],
+ [
+ -4.872,
+ -5.896
+ ],
+ [
+ -5.039,
+ -6.056
+ ],
+ [
+ -5.039,
+ -4.996
+ ]
+ ],
+ "c": true
+ }
+ ],
+ "e": [
+ {
+ "i": [
+ [
+ 0.192,
+ 0
+ ],
+ [
+ 0.146,
+ 0.147
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.293,
+ 0.293
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.293,
+ -0.293
+ ]
+ ],
+ "o": [
+ [
+ -0.192,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0.293,
+ -0.293
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.293,
+ 0.293
+ ],
+ [
+ -0.146,
+ 0.147
+ ]
+ ],
+ "v": [
+ [
+ 5.402,
+ 6.189
+ ],
+ [
+ 4.872,
+ 5.97
+ ],
+ [
+ -5.932,
+ -4.836
+ ],
+ [
+ -5.932,
+ -5.896
+ ],
+ [
+ -4.872,
+ -5.896
+ ],
+ [
+ 5.932,
+ 4.91
+ ],
+ [
+ 5.932,
+ 5.97
+ ]
+ ],
+ "c": true
+ }
+ ]
+ },
+ {
+ "i": {
+ "x": 0.667,
+ "y": 1
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.167
+ },
+ "n": "0p667_1_0p167_0p167",
+ "t": 25,
+ "s": [
+ {
+ "i": [
+ [
+ 0.192,
+ 0
+ ],
+ [
+ 0.146,
+ 0.147
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.293,
+ 0.293
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.293,
+ -0.293
+ ]
+ ],
+ "o": [
+ [
+ -0.192,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0.293,
+ -0.293
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.293,
+ 0.293
+ ],
+ [
+ -0.146,
+ 0.147
+ ]
+ ],
+ "v": [
+ [
+ 5.402,
+ 6.189
+ ],
+ [
+ 4.872,
+ 5.97
+ ],
+ [
+ -5.932,
+ -4.836
+ ],
+ [
+ -5.932,
+ -5.896
+ ],
+ [
+ -4.872,
+ -5.896
+ ],
+ [
+ 5.932,
+ 4.91
+ ],
+ [
+ 5.932,
+ 5.97
+ ]
+ ],
+ "c": true
+ }
+ ],
+ "e": [
+ {
+ "i": [
+ [
+ 0.192,
+ 0
+ ],
+ [
+ 0.146,
+ 0.147
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.293,
+ 0.293
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.293,
+ -0.293
+ ]
+ ],
+ "o": [
+ [
+ -0.192,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0.293,
+ -0.293
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.293,
+ 0.293
+ ],
+ [
+ -0.146,
+ 0.147
+ ]
+ ],
+ "v": [
+ [
+ 5.402,
+ 6.189
+ ],
+ [
+ 4.872,
+ 5.97
+ ],
+ [
+ -5.932,
+ -4.836
+ ],
+ [
+ -5.932,
+ -5.896
+ ],
+ [
+ -4.872,
+ -5.896
+ ],
+ [
+ 5.932,
+ 4.91
+ ],
+ [
+ 5.932,
+ 5.97
+ ]
+ ],
+ "c": true
+ }
+ ]
+ },
+ {
+ "i": {
+ "x": 0.667,
+ "y": 1
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.167
+ },
+ "n": "0p667_1_0p167_0p167",
+ "t": 60,
+ "s": [
+ {
+ "i": [
+ [
+ 0.192,
+ 0
+ ],
+ [
+ 0.146,
+ 0.147
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.293,
+ 0.293
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.293,
+ -0.293
+ ]
+ ],
+ "o": [
+ [
+ -0.192,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0.293,
+ -0.293
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.293,
+ 0.293
+ ],
+ [
+ -0.146,
+ 0.147
+ ]
+ ],
+ "v": [
+ [
+ 5.402,
+ 6.189
+ ],
+ [
+ 4.872,
+ 5.97
+ ],
+ [
+ -5.932,
+ -4.836
+ ],
+ [
+ -5.932,
+ -5.896
+ ],
+ [
+ -4.872,
+ -5.896
+ ],
+ [
+ 5.932,
+ 4.91
+ ],
+ [
+ 5.932,
+ 5.97
+ ]
+ ],
+ "c": true
+ }
+ ],
+ "e": [
+ {
+ "i": [
+ [
+ 0.192,
+ 0
+ ],
+ [
+ 0.146,
+ 0.147
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.293,
+ 0.293
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.293,
+ -0.293
+ ]
+ ],
+ "o": [
+ [
+ -0.192,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.293,
+ -0.293
+ ],
+ [
+ 0.293,
+ -0.293
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.293,
+ 0.293
+ ],
+ [
+ -0.146,
+ 0.147
+ ]
+ ],
+ "v": [
+ [
+ -5.569,
+ -4.777
+ ],
+ [
+ -6.099,
+ -4.996
+ ],
+ [
+ -5.932,
+ -4.836
+ ],
+ [
+ -5.932,
+ -5.896
+ ],
+ [
+ -4.872,
+ -5.896
+ ],
+ [
+ -5.039,
+ -6.056
+ ],
+ [
+ -5.039,
+ -4.996
+ ]
+ ],
+ "c": true
+ }
+ ]
+ },
+ {
+ "t": 66.0000026882351
+ }
+ ],
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 6.475,
+ 6.439
+ ],
+ "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": 2,
+ "cix": 2,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 19.0000007738859,
+ "op": 66.0000026882351,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 6,
+ "ty": 4,
+ "nm": "mask_bar Outlines",
+ "td": 1,
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 10.585,
+ 9.499,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 7.488,
+ 7.402,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": 0.667,
+ "y": 1
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.167
+ },
+ "n": "0p667_1_0p167_0p167",
+ "t": 18,
+ "s": [
+ {
+ "i": [
+ [
+ 0.683,
+ 0.683
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.467,
+ 0
+ ],
+ [
+ 0.33,
+ -0.331
+ ],
+ [
+ 0,
+ -0.467
+ ],
+ [
+ -0.331,
+ -0.33
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.468,
+ 0
+ ],
+ [
+ -0.33,
+ 0.33
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.33,
+ -0.331
+ ],
+ [
+ -0.467,
+ 0
+ ],
+ [
+ -0.331,
+ 0.33
+ ],
+ [
+ 0,
+ 0.467
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.33,
+ 0.33
+ ],
+ [
+ 0.467,
+ 0
+ ],
+ [
+ 0.683,
+ -0.683
+ ]
+ ],
+ "v": [
+ [
+ -5.614,
+ -8.041
+ ],
+ [
+ -4.251,
+ -6.639
+ ],
+ [
+ -5.488,
+ -7.152
+ ],
+ [
+ -6.725,
+ -6.639
+ ],
+ [
+ -7.238,
+ -5.402
+ ],
+ [
+ -6.725,
+ -4.165
+ ],
+ [
+ -8.089,
+ -5.565
+ ],
+ [
+ -6.851,
+ -5.054
+ ],
+ [
+ -5.614,
+ -5.565
+ ]
+ ],
+ "c": true
+ }
+ ],
+ "e": [
+ {
+ "i": [
+ [
+ 0.683,
+ 0.683
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.467,
+ 0
+ ],
+ [
+ 0.33,
+ -0.331
+ ],
+ [
+ 0,
+ -0.467
+ ],
+ [
+ -0.331,
+ -0.33
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.468,
+ 0
+ ],
+ [
+ -0.33,
+ 0.33
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.33,
+ -0.331
+ ],
+ [
+ -0.467,
+ 0
+ ],
+ [
+ -0.331,
+ 0.33
+ ],
+ [
+ 0,
+ 0.467
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.33,
+ 0.33
+ ],
+ [
+ 0.467,
+ 0
+ ],
+ [
+ 0.683,
+ -0.683
+ ]
+ ],
+ "v": [
+ [
+ 6.555,
+ 4.165
+ ],
+ [
+ -4.251,
+ -6.639
+ ],
+ [
+ -5.488,
+ -7.152
+ ],
+ [
+ -6.725,
+ -6.639
+ ],
+ [
+ -7.238,
+ -5.402
+ ],
+ [
+ -6.725,
+ -4.165
+ ],
+ [
+ 4.079,
+ 6.641
+ ],
+ [
+ 5.318,
+ 7.152
+ ],
+ [
+ 6.555,
+ 6.641
+ ]
+ ],
+ "c": true
+ }
+ ]
+ },
+ {
+ "i": {
+ "x": 0.667,
+ "y": 1
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.167
+ },
+ "n": "0p667_1_0p167_0p167",
+ "t": 24,
+ "s": [
+ {
+ "i": [
+ [
+ 0.683,
+ 0.683
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.467,
+ 0
+ ],
+ [
+ 0.33,
+ -0.331
+ ],
+ [
+ 0,
+ -0.467
+ ],
+ [
+ -0.331,
+ -0.33
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.468,
+ 0
+ ],
+ [
+ -0.33,
+ 0.33
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.33,
+ -0.331
+ ],
+ [
+ -0.467,
+ 0
+ ],
+ [
+ -0.331,
+ 0.33
+ ],
+ [
+ 0,
+ 0.467
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.33,
+ 0.33
+ ],
+ [
+ 0.467,
+ 0
+ ],
+ [
+ 0.683,
+ -0.683
+ ]
+ ],
+ "v": [
+ [
+ 6.555,
+ 4.165
+ ],
+ [
+ -4.251,
+ -6.639
+ ],
+ [
+ -5.488,
+ -7.152
+ ],
+ [
+ -6.725,
+ -6.639
+ ],
+ [
+ -7.238,
+ -5.402
+ ],
+ [
+ -6.725,
+ -4.165
+ ],
+ [
+ 4.079,
+ 6.641
+ ],
+ [
+ 5.318,
+ 7.152
+ ],
+ [
+ 6.555,
+ 6.641
+ ]
+ ],
+ "c": true
+ }
+ ],
+ "e": [
+ {
+ "i": [
+ [
+ 0.683,
+ 0.683
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.467,
+ 0
+ ],
+ [
+ 0.33,
+ -0.331
+ ],
+ [
+ 0,
+ -0.467
+ ],
+ [
+ -0.331,
+ -0.33
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.468,
+ 0
+ ],
+ [
+ -0.33,
+ 0.33
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.33,
+ -0.331
+ ],
+ [
+ -0.467,
+ 0
+ ],
+ [
+ -0.331,
+ 0.33
+ ],
+ [
+ 0,
+ 0.467
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.33,
+ 0.33
+ ],
+ [
+ 0.467,
+ 0
+ ],
+ [
+ 0.683,
+ -0.683
+ ]
+ ],
+ "v": [
+ [
+ 6.555,
+ 4.165
+ ],
+ [
+ -4.251,
+ -6.639
+ ],
+ [
+ -5.488,
+ -7.152
+ ],
+ [
+ -6.725,
+ -6.639
+ ],
+ [
+ -7.238,
+ -5.402
+ ],
+ [
+ -6.725,
+ -4.165
+ ],
+ [
+ 4.079,
+ 6.641
+ ],
+ [
+ 5.318,
+ 7.152
+ ],
+ [
+ 6.555,
+ 6.641
+ ]
+ ],
+ "c": true
+ }
+ ]
+ },
+ {
+ "i": {
+ "x": 0.667,
+ "y": 1
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.167
+ },
+ "n": "0p667_1_0p167_0p167",
+ "t": 60,
+ "s": [
+ {
+ "i": [
+ [
+ 0.683,
+ 0.683
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.467,
+ 0
+ ],
+ [
+ 0.33,
+ -0.331
+ ],
+ [
+ 0,
+ -0.467
+ ],
+ [
+ -0.331,
+ -0.33
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.468,
+ 0
+ ],
+ [
+ -0.33,
+ 0.33
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.33,
+ -0.331
+ ],
+ [
+ -0.467,
+ 0
+ ],
+ [
+ -0.331,
+ 0.33
+ ],
+ [
+ 0,
+ 0.467
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.33,
+ 0.33
+ ],
+ [
+ 0.467,
+ 0
+ ],
+ [
+ 0.683,
+ -0.683
+ ]
+ ],
+ "v": [
+ [
+ 6.555,
+ 4.165
+ ],
+ [
+ -4.251,
+ -6.639
+ ],
+ [
+ -5.488,
+ -7.152
+ ],
+ [
+ -6.725,
+ -6.639
+ ],
+ [
+ -7.238,
+ -5.402
+ ],
+ [
+ -6.725,
+ -4.165
+ ],
+ [
+ 4.079,
+ 6.641
+ ],
+ [
+ 5.318,
+ 7.152
+ ],
+ [
+ 6.555,
+ 6.641
+ ]
+ ],
+ "c": true
+ }
+ ],
+ "e": [
+ {
+ "i": [
+ [
+ 0.683,
+ 0.683
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.467,
+ 0
+ ],
+ [
+ 0.33,
+ -0.331
+ ],
+ [
+ 0,
+ -0.467
+ ],
+ [
+ -0.331,
+ -0.33
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.468,
+ 0
+ ],
+ [
+ -0.33,
+ 0.33
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.33,
+ -0.331
+ ],
+ [
+ -0.467,
+ 0
+ ],
+ [
+ -0.331,
+ 0.33
+ ],
+ [
+ 0,
+ 0.467
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.33,
+ 0.33
+ ],
+ [
+ 0.467,
+ 0
+ ],
+ [
+ 0.683,
+ -0.683
+ ]
+ ],
+ "v": [
+ [
+ -5.614,
+ -8.041
+ ],
+ [
+ -4.251,
+ -6.639
+ ],
+ [
+ -5.488,
+ -7.152
+ ],
+ [
+ -6.725,
+ -6.639
+ ],
+ [
+ -7.238,
+ -5.402
+ ],
+ [
+ -6.725,
+ -4.165
+ ],
+ [
+ -8.089,
+ -5.565
+ ],
+ [
+ -6.851,
+ -5.054
+ ],
+ [
+ -5.614,
+ -5.565
+ ]
+ ],
+ "c": true
+ }
+ ]
+ },
+ {
+ "t": 66.0000026882351
+ }
+ ],
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 7.488,
+ 7.403
+ ],
+ "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": 2,
+ "cix": 2,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": -7.00000028511585,
+ "op": 91.000003706506,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 7,
+ "ty": 4,
+ "nm": "speaker Outlines",
+ "tt": 2,
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": 0.936,
+ "y": 1
+ },
+ "o": {
+ "x": 0.237,
+ "y": 0.075
+ },
+ "n": "0p936_1_0p237_0p075",
+ "t": 8,
+ "s": [
+ 5.049,
+ 9.975,
+ 0
+ ],
+ "e": [
+ 9.924,
+ 9.975,
+ 0
+ ],
+ "to": [
+ 0.8125,
+ 0,
+ 0
+ ],
+ "ti": [
+ -0.8125,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.936,
+ "y": 0.936
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.167
+ },
+ "n": "0p936_0p936_0p167_0p167",
+ "t": 18,
+ "s": [
+ 9.924,
+ 9.975,
+ 0
+ ],
+ "e": [
+ 9.924,
+ 9.975,
+ 0
+ ],
+ "to": [
+ 0,
+ 0,
+ 0
+ ],
+ "ti": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.833
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0
+ },
+ "n": "0p833_0p833_0p167_0",
+ "t": 66,
+ "s": [
+ 9.924,
+ 9.975,
+ 0
+ ],
+ "e": [
+ 5.049,
+ 9.975,
+ 0
+ ],
+ "to": [
+ -0.8125,
+ 0,
+ 0
+ ],
+ "ti": [
+ 0.8125,
+ 0,
+ 0
+ ]
+ },
+ {
+ "t": 76.0000030955435
+ }
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 5.051,
+ 7.326,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 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.132,
+ -0.106
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.17,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.17,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.132,
+ 0.106
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ -3.301,
+ 1.976
+ ],
+ [
+ -0.45,
+ 1.976
+ ],
+ [
+ 0.018,
+ 2.14
+ ],
+ [
+ 3.301,
+ 4.765
+ ],
+ [
+ 3.301,
+ -4.716
+ ],
+ [
+ 0.018,
+ -2.09
+ ],
+ [
+ -0.45,
+ -1.926
+ ],
+ [
+ -3.301,
+ -1.926
+ ]
+ ],
+ "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": [
+ [
+ 0.11,
+ 0
+ ],
+ [
+ 0.136,
+ 0.109
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0.414
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.414,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.26,
+ -0.124
+ ],
+ [
+ 0,
+ -0.289
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.26,
+ -0.124
+ ]
+ ],
+ "o": [
+ [
+ -0.167,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.414,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -0.414
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.225,
+ -0.181
+ ],
+ [
+ 0.26,
+ 0.124
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0.289
+ ],
+ [
+ -0.104,
+ 0.051
+ ]
+ ],
+ "v": [
+ [
+ 4.051,
+ 7.076
+ ],
+ [
+ 3.582,
+ 6.912
+ ],
+ [
+ -0.713,
+ 3.476
+ ],
+ [
+ -4.051,
+ 3.476
+ ],
+ [
+ -4.801,
+ 2.726
+ ],
+ [
+ -4.801,
+ -2.676
+ ],
+ [
+ -4.051,
+ -3.426
+ ],
+ [
+ -0.713,
+ -3.426
+ ],
+ [
+ 3.582,
+ -6.862
+ ],
+ [
+ 4.376,
+ -6.952
+ ],
+ [
+ 4.801,
+ -6.276
+ ],
+ [
+ 4.801,
+ 6.326
+ ],
+ [
+ 4.376,
+ 7.001
+ ]
+ ],
+ "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": [
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 5.051,
+ 7.326
+ ],
+ "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,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 91.000003706506,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
\ 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/colors.xml b/app/src/main/res/values/colors.xml
index 6dc696d2..88bce06d 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -20,7 +20,7 @@
#B00020
#FFFFFF
#000000
- #FFFFFF
+ #000000
#000000
#FFFFFF
\ 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 38a1c4dc..b4505143 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -55,4 +55,6 @@
TotalComposeActivity
Failed Google Login
%s년 %s월 %s일
+ sound_lottie.json
+ 속도 조절
\ No newline at end of file