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