diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterRepository.kt new file mode 100644 index 0000000..3f29f7b --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterRepository.kt @@ -0,0 +1,82 @@ +package dev.datlag.aniflow.anilist + +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.api.Optional +import dev.datlag.aniflow.anilist.model.Character +import kotlinx.coroutines.flow.* + +class CharacterRepository( + private val client: ApolloClient, + private val fallbackClient: ApolloClient +) { + + private val id = MutableStateFlow(null) + private val query = id.filterNotNull().map { + Query(it) + } + private val fallbackQuery = query.transform { + return@transform emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) + }.mapNotNull { + val data = it.data + if (data == null) { + if (it.hasErrors()) { + State.fromGraphQL(data) + } else { + null + } + } else { + State.fromGraphQL(data) + } + } + + val character = query.transform { + return@transform emitAll(client.query(it.toGraphQL()).toFlow()) + }.mapNotNull { + val data = it.data + if (data == null) { + if (it.hasErrors()) { + State.fromGraphQL(data) + } else { + null + } + } else { + State.fromGraphQL(data) + } + }.transform { + return@transform if (it is Error) { + emitAll(fallbackQuery) + } else { + emit(it) + } + } + + fun clear() = id.update { null } + + fun load(id: Int) = this.id.update { id } + + private data class Query( + val id: Int + ) { + fun toGraphQL() = CharacterQuery( + id = Optional.present(id), + html = Optional.present(true) + ) + } + + sealed interface State { + data class Success(val character: Character) : State + data object Error : State + + companion object { + fun fromGraphQL(query: CharacterQuery.Data?): State { + val char = query?.Character?.let { Character(it) } + + if (char == null) { + return Error + } + + return Success(char) + } + } + } +} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterStateMachine.kt deleted file mode 100644 index 498557e..0000000 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterStateMachine.kt +++ /dev/null @@ -1,111 +0,0 @@ -package dev.datlag.aniflow.anilist - -import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.api.Optional -import com.freeletics.flowredux.dsl.FlowReduxStateMachine -import dev.datlag.aniflow.anilist.model.Character -import dev.datlag.aniflow.firebase.FirebaseFactory -import dev.datlag.aniflow.model.CatchResult -import dev.datlag.aniflow.model.mapError -import dev.datlag.aniflow.model.safeFirstOrNull -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlin.time.Duration.Companion.seconds - -@OptIn(ExperimentalCoroutinesApi::class) -class CharacterStateMachine( - private val client: ApolloClient, - private val fallbackClient: ApolloClient, - private val crashlytics: FirebaseFactory.Crashlytics?, - private val id: Int -) : FlowReduxStateMachine( - initialState = State.Loading(id) -) { - - var currentState: State = State.Loading(id) - private set - - init { - spec { - inState { - onEnterEffect { - currentState = it - } - onEnter { state -> - val response = CatchResult.repeat(2, timeoutDuration = 30.seconds) { - val query = client.query(state.snapshot.query) - - query.execute().data ?: query.toFlow().safeFirstOrNull()?.dataOrThrow() - }.mapError { - val query = fallbackClient.query(state.snapshot.query) - - query.execute().data ?: query.toFlow().safeFirstOrNull()?.data - }.mapSuccess { - it.Character?.let { data -> - Character(data)?.let { char -> - State.Success(state.snapshot.query, char) - } - } - } - - val cached = Cache.getCharacter(state.snapshot.query) - - state.override { - response.asSuccess { - crashlytics?.log(it) - - if (cached != null) { - State.Success(query, cached) - } else { - State.Error(query) - } - } - } - } - } - inState { - onEnterEffect { - Cache.setCharacter(it.query, it.character) - currentState = it - } - } - inState { - onEnterEffect { - currentState = it - } - on { _, state -> - state.override { - State.Loading(state.snapshot.query) - } - } - } - } - } - - sealed interface State { - val isLoading: Boolean - get() = this is Loading - - val isSuccess: Boolean - get() = this is Success - - data class Loading(internal val query: CharacterQuery) : State { - constructor(id: Int) : this( - query = CharacterQuery( - id = Optional.present(id), - html = Optional.present(true) - ) - ) - } - data class Success( - internal val query: CharacterQuery, - val character: Character - ) : State - data class Error( - internal val query: CharacterQuery, - ) : State - } - - sealed interface Action { - data object Retry : Action - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt index e769b92..0c048ff 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -155,5 +155,11 @@ data object NetworkModule { nsfw = appSettings.adultContent ) } + bindSingleton { + CharacterRepository( + client = instance(Constants.AniList.APOLLO_CLIENT), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), + ) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterComponent.kt index 0f26149..3059b41 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterComponent.kt @@ -1,6 +1,6 @@ package dev.datlag.aniflow.ui.navigation.screen.medium.dialog.character -import dev.datlag.aniflow.anilist.CharacterStateMachine +import dev.datlag.aniflow.anilist.CharacterRepository import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.settings.model.CharLanguage import dev.datlag.aniflow.ui.navigation.DialogComponent @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.StateFlow interface CharacterComponent : DialogComponent { val initialChar: Character - val state: StateFlow + val state: Flow val charLanguage: Flow val image: Flow diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt index 1d905a2..4fcc449 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt @@ -24,7 +24,7 @@ import coil3.compose.AsyncImage import coil3.compose.rememberAsyncImagePainter import dev.datlag.aniflow.LocalEdgeToEdge import dev.datlag.aniflow.SharedRes -import dev.datlag.aniflow.anilist.CharacterStateMachine +import dev.datlag.aniflow.anilist.CharacterRepository import dev.datlag.aniflow.common.* import dev.datlag.aniflow.ui.navigation.screen.medium.component.TranslateButton import dev.datlag.tooling.compose.ifFalse @@ -60,7 +60,7 @@ fun CharacterDialog(component: CharacterComponent) { contentAlignment = Alignment.Center ) { val image by component.image.collectAsStateWithLifecycle(component.initialChar.image) - val state by component.state.collectAsStateWithLifecycle() + val state by component.state.collectAsStateWithLifecycle(null) this@ModalBottomSheet.AnimatedVisibility( modifier = Modifier.align(Alignment.CenterStart), @@ -94,7 +94,7 @@ fun CharacterDialog(component: CharacterComponent) { ) this@ModalBottomSheet.AnimatedVisibility( - visible = state.isLoading, + visible = state == null, enter = fadeIn(), exit = fadeOut() ) { @@ -105,7 +105,7 @@ fun CharacterDialog(component: CharacterComponent) { this@ModalBottomSheet.AnimatedVisibility( modifier = Modifier.align(Alignment.CenterEnd), - visible = state.isSuccess, + visible = state is CharacterRepository.State.Success, enter = fadeIn(), exit = fadeOut() ) { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialogComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialogComponent.kt index 9225c44..26125fe 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialogComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialogComponent.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.Optional import com.arkivanov.decompose.ComponentContext -import dev.datlag.aniflow.anilist.CharacterStateMachine +import dev.datlag.aniflow.anilist.CharacterRepository import dev.datlag.aniflow.anilist.FavoriteToggleMutation import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.common.nullableFirebaseInstance @@ -29,26 +29,14 @@ class CharacterDialogComponent( ) : CharacterComponent, ComponentContext by componentContext { private val aniListClient by di.instance(Constants.AniList.APOLLO_CLIENT) - private val aniListFallbackClient by di.instance(Constants.AniList.FALLBACK_APOLLO_CLIENT) - private val characterStateMachine = CharacterStateMachine( - client = aniListClient, - fallbackClient = aniListFallbackClient, - crashlytics = di.nullableFirebaseInstance()?.crashlytics, - id = initialChar.id - ) + private val characterRepository by di.instance() private val appSettings by di.instance() override val charLanguage: Flow = appSettings.charLanguage.flowOn(ioDispatcher()) - override val state = characterStateMachine.state.flowOn( - context = ioDispatcher() - ).stateIn( - scope = ioScope(), - started = SharingStarted.WhileSubscribed(), - initialValue = characterStateMachine.currentState - ) + override val state = characterRepository.character private val characterSuccessState = state.mapNotNull { - it.safeCast() + it.safeCast() } override val image: Flow = characterSuccessState.map { @@ -85,6 +73,10 @@ class CharacterDialogComponent( it.character.isFavoriteBlocked } + init { + characterRepository.load(initialChar.id) + } + @Composable override fun render() { onRender { @@ -101,9 +93,7 @@ class CharacterDialogComponent( } override fun retry() { - launchIO { - characterStateMachine.dispatch(CharacterStateMachine.Action.Retry) - } + } override fun toggleFavorite() {