blob: 17b70c4c9d55045f782d229506957aff22c28a57 [file] [log] [blame]
package com.airbnb.lottie.sample.compose.lottiefiles
import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Repeat
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.navigate
import com.airbnb.lottie.sample.compose.R
import com.airbnb.lottie.sample.compose.Route
import com.airbnb.lottie.sample.compose.api.AnimationDataV2
import com.airbnb.lottie.sample.compose.api.LottieFilesApi
import com.airbnb.lottie.sample.compose.composables.AnimationRow
import com.airbnb.lottie.sample.compose.dagger.AssistedViewModelFactory
import com.airbnb.lottie.sample.compose.dagger.daggerMavericksViewModelFactory
import com.airbnb.lottie.sample.compose.utils.collectState
import com.airbnb.lottie.sample.compose.utils.findNavController
import com.airbnb.lottie.sample.compose.utils.mavericksViewModel
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
data class LottieFilesSearchState(
val query: String = "",
val results: List<AnimationDataV2> = emptyList(),
val currentPage: Int = 1,
val lastPage: Int = 0,
val fetchException: Boolean = false,
) : MavericksState
class LottieFilesSearchViewModel @AssistedInject constructor(
@Assisted initialState: LottieFilesSearchState,
private val api: LottieFilesApi,
) : MavericksViewModel<LottieFilesSearchState>(initialState) {
private var fetchJob: Job? = null
init {
onEach(LottieFilesSearchState::query) { query ->
fetchJob?.cancel()
if (query.isBlank()) {
setState { copy(results = emptyList(), currentPage = 1, lastPage = 1, fetchException = false) }
} else {
fetchJob = viewModelScope.launch {
val results = try {
api.search(query, 1)
} catch (e: Exception) {
setState { copy(fetchException = true) }
return@launch
}
setState {
copy(
results = results.data.map(::AnimationDataV2),
currentPage = results.current_page,
lastPage = results.last_page,
fetchException = false
)
}
}
}
}
}
fun fetchNextPage() = withState { state ->
fetchJob?.cancel()
if (state.currentPage >= state.lastPage) return@withState
fetchJob = viewModelScope.launch {
val response = try {
api.search(state.query, state.currentPage + 1)
} catch (e: Exception) {
setState { copy(fetchException = true) }
return@launch
}
setState {
copy(
results = results + response.data.map(::AnimationDataV2),
currentPage = response.current_page,
fetchException = false
)
}
}
}
fun setQuery(query: String) = setState { copy(query = query, currentPage = 1, results = emptyList()) }
@AssistedFactory
interface Factory : AssistedViewModelFactory<LottieFilesSearchViewModel, LottieFilesSearchState> {
override fun create(initialState: LottieFilesSearchState): LottieFilesSearchViewModel
}
companion object : MavericksViewModelFactory<LottieFilesSearchViewModel, LottieFilesSearchState> by daggerMavericksViewModelFactory()
}
@Composable
fun LottieFilesSearchPage() {
val viewModel: LottieFilesSearchViewModel = mavericksViewModel()
val state = viewModel.collectState()
val navController = findNavController()
LottieFilesSearchPage(
state,
viewModel::setQuery,
viewModel::fetchNextPage,
onAnimationClicked = { data ->
navController.navigate(Route.Player.forUrl(data.file, backgroundColor = data.bg_color))
}
)
}
@Composable
fun LottieFilesSearchPage(
state: LottieFilesSearchState,
onQueryChanged: (String) -> Unit,
fetchNextPage: () -> Unit,
onAnimationClicked: (AnimationDataV2) -> Unit,
modifier: Modifier = Modifier,
) {
Box {
Column(
modifier = Modifier.then(modifier)
) {
OutlinedTextField(
value = state.query,
onValueChange = onQueryChanged,
label = { Text(stringResource(R.string.query)) },
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
)
LazyColumn(
modifier = Modifier.weight(1f)
) {
itemsIndexed(state.results) { index, result ->
if (index == state.results.size - 1) {
SideEffect(fetchNextPage)
}
AnimationRow(
title = result.title,
previewUrl = result.preview_url ?: "",
previewBackgroundColor = result.bgColor,
onClick = { onAnimationClicked(result) }
)
}
}
}
if (state.fetchException) {
FloatingActionButton(
onClick = fetchNextPage,
content = {
Icon(
imageVector = Icons.Filled.Repeat,
tint = Color.White,
contentDescription = null
)
},
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 24.dp)
)
}
}
}
@Preview
@Composable
fun PreviewSearchPage() {
val data = AnimationDataV2(0, null, "https://assets9.lottiefiles.com/render/k1821vf5.png", "Loading", "")
val state = LottieFilesSearchState(
results = listOf(data, data, data),
fetchException = true
)
Surface(color = Color.White) {
LottieFilesSearchPage(
state = state,
onQueryChanged = {},
fetchNextPage = {},
onAnimationClicked = {}
)
}
}