From 03a0168aca36306c4e7ff0ab36568d27debffea1 Mon Sep 17 00:00:00 2001 From: DatLag Date: Tue, 21 May 2024 22:39:05 +0200 Subject: [PATCH] initial repository re-write with flowredux --- ...ngQuery.graphql => PageMediaQuery.graphql} | 20 +-- .../commonMain/graphql/SeasonQuery.graphql | 108 ----------------- .../anilist/PopularNextSeasonRepository.kt | 110 ----------------- .../anilist/PopularNextSeasonStateMachine.kt | 98 +++++++++++++++ .../anilist/PopularSeasonRepository.kt | 110 ----------------- .../anilist/PopularSeasonStateMachine.kt | 98 +++++++++++++++ .../dev/datlag/aniflow/anilist/StateSaver.kt | 9 ++ .../aniflow/anilist/TrendingRepository.kt | 114 ------------------ .../aniflow/anilist/TrendingStateMachine.kt | 101 ++++++++++++++++ .../aniflow/anilist/common/ExtendDateTime.kt | 20 +-- .../aniflow/anilist/common/ExtendOptional.kt | 41 +++++++ .../datlag/aniflow/anilist/model/Character.kt | 52 +------- .../datlag/aniflow/anilist/model/Medium.kt | 82 +------------ .../aniflow/anilist/model/PageMediaQuery.kt | 81 +++++++++++++ .../aniflow/anilist/state/CollectionState.kt | 21 ---- .../aniflow/anilist/state/HomeDefaultState.kt | 59 +++++++++ .../aniflow/common/ExtendMedium.android.kt | 2 - .../datlag/aniflow/module/NetworkModule.kt | 63 +++++----- .../dev/datlag/aniflow/other/StateSaver.kt | 8 +- .../navigation/screen/home/HomeComponent.kt | 9 +- .../screen/home/HomeScreenComponent.kt | 40 +++--- .../screen/home/component/DefaultOverview.kt | 13 +- 22 files changed, 579 insertions(+), 680 deletions(-) rename anilist/src/commonMain/graphql/{TrendingQuery.graphql => PageMediaQuery.graphql} (90%) delete mode 100644 anilist/src/commonMain/graphql/SeasonQuery.graphql delete mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonStateMachine.kt delete mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonStateMachine.kt create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt delete mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingStateMachine.kt create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/common/ExtendOptional.kt create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/PageMediaQuery.kt create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/HomeDefaultState.kt diff --git a/anilist/src/commonMain/graphql/TrendingQuery.graphql b/anilist/src/commonMain/graphql/PageMediaQuery.graphql similarity index 90% rename from anilist/src/commonMain/graphql/TrendingQuery.graphql rename to anilist/src/commonMain/graphql/PageMediaQuery.graphql index b825002..ea704ce 100644 --- a/anilist/src/commonMain/graphql/TrendingQuery.graphql +++ b/anilist/src/commonMain/graphql/PageMediaQuery.graphql @@ -1,19 +1,23 @@ -query TrendingQuery( +query PageMediaQuery( $page: Int, $perPage: Int, - $type: MediaType, - $statusVersion: Int, - $html: Boolean, $sort: [MediaSort], + $year: Int, + $season: MediaSeason, + $preventGenres: [String], + $type: MediaType, $adultContent: Boolean, - $preventGenres: [String] + $statusVersion: Int!, + $html: Boolean!, ) { Page(page: $page, perPage: $perPage) { media( - type: $type, sort: $sort, - isAdult: $adultContent, - genre_not_in: $preventGenres + seasonYear: $year, + season: $season, + genre_not_in: $preventGenres, + type: $type, + isAdult: $adultContent ) { id, idMal, diff --git a/anilist/src/commonMain/graphql/SeasonQuery.graphql b/anilist/src/commonMain/graphql/SeasonQuery.graphql deleted file mode 100644 index 26c65d7..0000000 --- a/anilist/src/commonMain/graphql/SeasonQuery.graphql +++ /dev/null @@ -1,108 +0,0 @@ -query SeasonQuery( - $page: Int, - $perPage: Int, - $sort: [MediaSort], - $year: Int, - $season: MediaSeason, - $preventGenres: [String], - $type: MediaType, - $adultContent: Boolean, - $statusVersion: Int, - $html: Boolean, -) { - Page(page: $page, perPage: $perPage) { - media(sort: $sort, seasonYear: $year, season: $season, genre_not_in: $preventGenres, type: $type, isAdult: $adultContent) { - id, - idMal, - type, - status(version: $statusVersion), - description(asHtml: $html), - episodes, - duration, - chapters, - countryOfOrigin, - popularity, - isFavourite, - isFavouriteBlocked, - isAdult, - format, - bannerImage, - coverImage { - extraLarge, - large, - medium, - color - }, - averageScore, - title { - english, - native, - romaji, - userPreferred - }, - nextAiringEpisode { - episode, - airingAt - }, - rankings { - rank, - allTime, - year, - season, - type - }, - genres, - characters(sort: [FAVOURITES_DESC,RELEVANCE]) { - nodes { - id, - name { - first - middle - last - full - native - userPreferred - }, - image { - large - medium - }, - description(asHtml:$html) - gender, - dateOfBirth { - year - month - day - }, - bloodType, - isFavourite, - isFavouriteBlocked, - } - }, - mediaListEntry { - score(format: POINT_5), - status, - progress, - repeat, - startedAt { - year, - month, - day - } - }, - trailer { - id, - site, - thumbnail - }, - siteUrl, - chapters, - volumes, - startDate { - year, - month, - day - } - } - } -} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt deleted file mode 100644 index 35ba539..0000000 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt +++ /dev/null @@ -1,110 +0,0 @@ -package dev.datlag.aniflow.anilist - -import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.api.Optional -import dev.datlag.aniflow.anilist.common.nextSeason -import dev.datlag.aniflow.anilist.state.CollectionState -import dev.datlag.aniflow.anilist.type.MediaSeason -import dev.datlag.aniflow.anilist.type.MediaSort -import dev.datlag.aniflow.anilist.type.MediaType -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import kotlinx.datetime.Clock - -class PopularNextSeasonRepository( - private val apolloClient: ApolloClient, - private val fallbackClient: ApolloClient, - private val nsfw: Flow = flowOf(false), -) { - - private val page = MutableStateFlow(0) - - private val query = combine(page, nsfw.distinctUntilChanged()) { p, n -> - val (season, year) = Clock.System.now().nextSeason - - Query( - page = p, - nsfw = n, - season = season, - year = year - ) - }.distinctUntilChanged() - - @OptIn(ExperimentalCoroutinesApi::class) - private val fallbackQuery = query.transformLatest { - return@transformLatest emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) - }.mapNotNull { - val data = it.data - if (data == null) { - if (it.hasErrors()) { - CollectionState.fromSeasonGraphQL(data) - } else { - null - } - } else { - CollectionState.fromSeasonGraphQL(data) - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - val popularNextSeason = query.transformLatest { - return@transformLatest emitAll(apolloClient.query(it.toGraphQL()).toFlow()) - }.mapNotNull { - val data = it.data - if (data == null) { - if (it.hasErrors()) { - CollectionState.fromSeasonGraphQL(data) - } else { - null - } - } else { - CollectionState.fromSeasonGraphQL(data) - } - }.transformLatest { - return@transformLatest if (it.isError) { - emitAll(fallbackQuery) - } else { - emit(it) - } - } - - fun nextPage() = page.getAndUpdate { - it + 1 - } - - fun previousPage() = page.getAndUpdate { - it - 1 - } - - private data class Query( - val page: Int, - val nsfw: Boolean, - val season: MediaSeason, - val year: Int - ) { - fun toGraphQL() = SeasonQuery( - page = Optional.present(page), - perPage = Optional.present(20), - adultContent = if (nsfw) { - Optional.absent() - } else { - Optional.present(nsfw) - }, - type = Optional.present(MediaType.ANIME), - sort = Optional.present(listOf(MediaSort.POPULARITY_DESC)), - preventGenres = if (nsfw) { - Optional.absent() - } else { - Optional.present(AdultContent.Genre.allTags) - }, - year = Optional.present(year), - season = if (season == MediaSeason.UNKNOWN__) { - Optional.absent() - } else { - Optional.present(season) - }, - statusVersion = Optional.present(2), - html = Optional.present(true) - ) - } -} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonStateMachine.kt new file mode 100644 index 0000000..5d2c428 --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonStateMachine.kt @@ -0,0 +1,98 @@ +package dev.datlag.aniflow.anilist + +import com.apollographql.apollo3.ApolloClient +import com.freeletics.flowredux.dsl.FlowReduxStateMachine +import dev.datlag.aniflow.anilist.model.PageMediaQuery +import dev.datlag.aniflow.anilist.state.HomeDefaultAction +import dev.datlag.aniflow.anilist.state.HomeDefaultState +import dev.datlag.aniflow.anilist.type.MediaType +import dev.datlag.aniflow.firebase.FirebaseFactory +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapLatest + +@OptIn(ExperimentalCoroutinesApi::class) +class PopularNextSeasonStateMachine( + private val client: ApolloClient, + private val fallbackClient: ApolloClient, + private val nsfw: Flow, + private val viewManga: Flow, + private val crashlytics: FirebaseFactory.Crashlytics? +) : FlowReduxStateMachine( + initialState = currentState +) { + + var currentState: HomeDefaultState + get() = Companion.currentState + private set(value) { + Companion.currentState = value + } + + private val type = viewManga.mapLatest { + if (it) { + MediaType.MANGA + } else { + MediaType.ANIME + } + }.distinctUntilChanged() + + private val query = combine( + type, + nsfw.distinctUntilChanged() + ) { t, n -> + PageMediaQuery.PopularNextSeason( + type = t, + nsfw = n + ) + }.distinctUntilChanged() + + init { + spec { + inState { + onEnterEffect { + currentState = it + } + collectWhileInState(query) { q, state -> + state.override { + HomeDefaultState.Loading( + query = q, + fallback = false + ) + } + } + } + inState { + collectWhileInState( + flowBuilder = { + val usedClient = if (it.fallback) { + fallbackClient + } else { + client + } + + usedClient.query(it.query.toGraphQL()).toFlow() + } + ) { response, state -> + state.override { + fromGraphQL(response) + } + } + } + inState { + onEnterEffect { + crashlytics?.log(it.throwable) + } + } + } + } + + companion object { + var currentState: HomeDefaultState + get() = StateSaver.popularNextSeasonState + private set(value) { + StateSaver.popularNextSeasonState = value + } + } +} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt deleted file mode 100644 index 120f9f9..0000000 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt +++ /dev/null @@ -1,110 +0,0 @@ -package dev.datlag.aniflow.anilist - -import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.api.Optional -import dev.datlag.aniflow.anilist.state.CollectionState -import dev.datlag.aniflow.anilist.type.MediaSort -import dev.datlag.aniflow.anilist.type.MediaType -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* - -class PopularSeasonRepository( - private val apolloClient: ApolloClient, - private val fallbackClient: ApolloClient, - private val nsfw: Flow = flowOf(false), - private val viewManga: Flow = flowOf(false), -) { - - private val page = MutableStateFlow(0) - - @OptIn(ExperimentalCoroutinesApi::class) - private val type = viewManga.distinctUntilChanged().mapLatest { - page.update { 0 } - if (it) { - MediaType.MANGA - } else { - MediaType.ANIME - } - } - - private val query = combine(page, type, nsfw.distinctUntilChanged()) { p, t, n -> - Query( - page = p, - type = t, - nsfw = n - ) - }.distinctUntilChanged() - - @OptIn(ExperimentalCoroutinesApi::class) - private val fallbackQuery = query.transformLatest { - return@transformLatest emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) - }.mapNotNull { - val data = it.data - if (data == null) { - if (it.hasErrors()) { - CollectionState.fromSeasonGraphQL(data) - } else { - null - } - } else { - CollectionState.fromSeasonGraphQL(data) - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - val popularThisSeason = query.transformLatest { - return@transformLatest emitAll(apolloClient.query(it.toGraphQL()).toFlow()) - }.mapNotNull { - val data = it.data - if (data == null) { - if (it.hasErrors()) { - CollectionState.fromSeasonGraphQL(data) - } else { - null - } - } else { - CollectionState.fromSeasonGraphQL(data) - } - }.transformLatest { - return@transformLatest if (it.isError) { - emitAll(fallbackQuery) - } else { - emit(it) - } - } - - fun nextPage() = page.getAndUpdate { - it + 1 - } - - fun previousPage() = page.getAndUpdate { - it - 1 - } - - private data class Query( - val page: Int, - val type: MediaType, - val nsfw: Boolean - ) { - fun toGraphQL() = SeasonQuery( - page = Optional.present(page), - perPage = Optional.present(20), - adultContent = if (nsfw) { - Optional.absent() - } else { - Optional.present(nsfw) - }, - type = Optional.present(type), - sort = Optional.present(listOf(MediaSort.POPULARITY_DESC)), - preventGenres = if (nsfw) { - Optional.absent() - } else { - Optional.present(AdultContent.Genre.allTags) - }, - year = Optional.absent(), - season = Optional.absent(), - statusVersion = Optional.present(2), - html = Optional.present(true) - ) - } -} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonStateMachine.kt new file mode 100644 index 0000000..94b315d --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonStateMachine.kt @@ -0,0 +1,98 @@ +package dev.datlag.aniflow.anilist + +import com.apollographql.apollo3.ApolloClient +import com.freeletics.flowredux.dsl.FlowReduxStateMachine +import dev.datlag.aniflow.anilist.model.PageMediaQuery +import dev.datlag.aniflow.anilist.state.HomeDefaultAction +import dev.datlag.aniflow.anilist.state.HomeDefaultState +import dev.datlag.aniflow.anilist.type.MediaType +import dev.datlag.aniflow.firebase.FirebaseFactory +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapLatest + +@OptIn(ExperimentalCoroutinesApi::class) +class PopularSeasonStateMachine( + private val client: ApolloClient, + private val fallbackClient: ApolloClient, + private val nsfw: Flow, + private val viewManga: Flow, + private val crashlytics: FirebaseFactory.Crashlytics? +) : FlowReduxStateMachine( + initialState = currentState +) { + + var currentState: HomeDefaultState + get() = Companion.currentState + private set(value) { + Companion.currentState = value + } + + private val type = viewManga.mapLatest { + if (it) { + MediaType.MANGA + } else { + MediaType.ANIME + } + }.distinctUntilChanged() + + private val query = combine( + type, + nsfw.distinctUntilChanged() + ) { t, n -> + PageMediaQuery.PopularSeason( + type = t, + nsfw = n + ) + }.distinctUntilChanged() + + init { + spec { + inState { + onEnterEffect { + currentState = it + } + collectWhileInState(query) { q, state -> + state.override { + HomeDefaultState.Loading( + query = q, + fallback = false + ) + } + } + } + inState { + collectWhileInState( + flowBuilder = { + val usedClient = if (it.fallback) { + fallbackClient + } else { + client + } + + usedClient.query(it.query.toGraphQL()).toFlow() + } + ) { response, state -> + state.override { + fromGraphQL(response) + } + } + } + inState { + onEnterEffect { + crashlytics?.log(it.throwable) + } + } + } + } + + companion object { + var currentState: HomeDefaultState + get() = StateSaver.popularSeasonState + private set(value) { + StateSaver.popularSeasonState = value + } + } +} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt new file mode 100644 index 0000000..72a5a2e --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt @@ -0,0 +1,9 @@ +package dev.datlag.aniflow.anilist + +import dev.datlag.aniflow.anilist.state.HomeDefaultState + +internal object StateSaver { + var trendingState: HomeDefaultState = HomeDefaultState.None + var popularSeasonState: HomeDefaultState = HomeDefaultState.None + var popularNextSeasonState: HomeDefaultState = HomeDefaultState.None +} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt deleted file mode 100644 index 1c9aea3..0000000 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt +++ /dev/null @@ -1,114 +0,0 @@ -package dev.datlag.aniflow.anilist - -import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.api.Optional -import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.anilist.state.CollectionState -import dev.datlag.aniflow.anilist.type.MediaSort -import dev.datlag.aniflow.anilist.type.MediaType -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import kotlinx.serialization.Serializable - -class TrendingRepository( - private val apolloClient: ApolloClient, - private val fallbackClient: ApolloClient, - private val nsfw: Flow = flowOf(false), - private val viewManga: Flow = flowOf(false), -) { - - private val page = MutableStateFlow(0) - - @OptIn(ExperimentalCoroutinesApi::class) - private val type = viewManga.distinctUntilChanged().mapLatest { - page.update { 0 } - if (it) { - MediaType.MANGA - } else { - MediaType.ANIME - } - } - - private val query = combine(page, type, nsfw.distinctUntilChanged()) { p, t, n -> - Query( - page = p, - type = t, - nsfw = n - ) - }.distinctUntilChanged() - - @OptIn(ExperimentalCoroutinesApi::class) - private val fallbackQuery = query.transformLatest { - return@transformLatest emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) - }.mapNotNull { - val data = it.data - if (data == null) { - if (it.hasErrors()) { - CollectionState.fromTrendingGraphQL(data) - } else { - null - } - } else { - CollectionState.fromTrendingGraphQL(data) - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - val trending = query.transformLatest { - return@transformLatest emitAll(apolloClient.query(it.toGraphQL()).toFlow()) - }.mapNotNull { - val data = it.data - if (data == null) { - if (it.hasErrors()) { - CollectionState.fromTrendingGraphQL(data) - } else { - null - } - } else { - CollectionState.fromTrendingGraphQL(data) - } - }.transformLatest { - return@transformLatest if (it.isError) { - emitAll(fallbackQuery) - } else { - emit(it) - } - } - - fun nextPage() = page.getAndUpdate { - it + 1 - } - - fun previousPage() = page.getAndUpdate { - it - 1 - } - - private data class Query( - val page: Int, - val type: MediaType, - val nsfw: Boolean - ) { - fun toGraphQL() = TrendingQuery( - page = Optional.present(page), - perPage = Optional.present(20), - adultContent = if (nsfw) { - Optional.absent() - } else { - Optional.present(nsfw) - }, - type = if (type == MediaType.UNKNOWN__) { - Optional.absent() - } else { - Optional.present(type) - }, - sort = Optional.present(listOf(MediaSort.TRENDING_DESC)), - preventGenres = if (nsfw) { - Optional.absent() - } else { - Optional.present(AdultContent.Genre.allTags) - }, - statusVersion = Optional.present(2), - html = Optional.present(true) - ) - } -} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingStateMachine.kt new file mode 100644 index 0000000..9fb9521 --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingStateMachine.kt @@ -0,0 +1,101 @@ +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.Medium +import dev.datlag.aniflow.anilist.model.PageMediaQuery +import dev.datlag.aniflow.anilist.state.HomeDefaultAction +import dev.datlag.aniflow.anilist.state.HomeDefaultState +import dev.datlag.aniflow.anilist.type.MediaSort +import dev.datlag.aniflow.anilist.type.MediaType +import dev.datlag.aniflow.firebase.FirebaseFactory +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapLatest + +@OptIn(ExperimentalCoroutinesApi::class) +class TrendingStateMachine( + private val client: ApolloClient, + private val fallbackClient: ApolloClient, + private val nsfw: Flow, + private val viewManga: Flow, + private val crashlytics: FirebaseFactory.Crashlytics? +) : FlowReduxStateMachine( + initialState = currentState +) { + + var currentState: HomeDefaultState + get() = Companion.currentState + private set(value) { + Companion.currentState = value + } + + private val type = viewManga.mapLatest { + if (it) { + MediaType.MANGA + } else { + MediaType.ANIME + } + }.distinctUntilChanged() + + private val query = combine( + type, + nsfw.distinctUntilChanged() + ) { t, n -> + PageMediaQuery.Trending( + type = t, + nsfw = n + ) + }.distinctUntilChanged() + + init { + spec { + inState { + onEnterEffect { + currentState = it + } + collectWhileInState(query) { q, state -> + state.override { + HomeDefaultState.Loading( + query = q, + fallback = false + ) + } + } + } + inState { + collectWhileInState( + flowBuilder = { + val usedClient = if (it.fallback) { + fallbackClient + } else { + client + } + + usedClient.query(it.query.toGraphQL()).toFlow() + } + ) { response, state -> + state.override { + fromGraphQL(response) + } + } + } + inState { + onEnterEffect { + crashlytics?.log(it.throwable) + } + } + } + } + + companion object { + var currentState: HomeDefaultState + get() = StateSaver.trendingState + private set(value) { + StateSaver.trendingState = value + } + } +} diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/common/ExtendDateTime.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/common/ExtendDateTime.kt index 12a100d..289789b 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/common/ExtendDateTime.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/common/ExtendDateTime.kt @@ -28,7 +28,7 @@ internal val Instant.nextSeason: Pair return date.month.season.next(date.date) } -internal fun TrendingQuery.StartDate.toLocalDate(): LocalDate? { +internal fun PageMediaQuery.StartDate.toLocalDate(): LocalDate? { return LocalDate( year = year ?: return null, monthNumber = month ?: return null, @@ -44,14 +44,6 @@ internal fun AiringQuery.StartDate.toLocalDate(): LocalDate? { ) } -internal fun SeasonQuery.StartDate.toLocalDate(): LocalDate? { - return LocalDate( - year = year ?: return null, - monthNumber = month ?: return null, - dayOfMonth = day ?: 1 - ) -} - internal fun MediumQuery.StartDate.toLocalDate(): LocalDate? { return LocalDate( year = year ?: return null, @@ -92,7 +84,7 @@ internal fun AiringQuery.StartedAt.toLocalDate(): LocalDate? { ) } -internal fun TrendingQuery.StartedAt.toLocalDate(): LocalDate? { +internal fun PageMediaQuery.StartedAt.toLocalDate(): LocalDate? { return LocalDate( year = year ?: return null, monthNumber = month ?: return null, @@ -108,14 +100,6 @@ internal fun MediumQuery.StartedAt.toLocalDate(): LocalDate? { ) } -internal fun SeasonQuery.StartedAt.toLocalDate(): LocalDate? { - return LocalDate( - year = year ?: return null, - monthNumber = month ?: return null, - dayOfMonth = day ?: 1 - ) -} - internal fun ListQuery.StartedAt.toLocalDate(): LocalDate? { return LocalDate( year = year ?: return null, diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/common/ExtendOptional.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/common/ExtendOptional.kt new file mode 100644 index 0000000..23a9312 --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/common/ExtendOptional.kt @@ -0,0 +1,41 @@ +package dev.datlag.aniflow.anilist.common + +import com.apollographql.apollo3.api.Optional +import dev.datlag.aniflow.anilist.type.MediaSeason +import dev.datlag.aniflow.anilist.type.MediaType + +fun Optional.Companion.presentIf(value: Boolean) = if (value) { + present(value) +} else { + absent() +} + +fun Optional.Companion.presentIfNot(value: Boolean) = if (value) { + absent() +} else { + present(value) +} + +fun Optional.Companion.presentMediaType(type: MediaType) = when (type) { + MediaType.UNKNOWN__ -> absent() + else -> present(type) +} + +fun Optional.Companion.presentMediaSeason(type: MediaSeason) = when (type) { + MediaSeason.UNKNOWN__ -> absent() + else -> present(type) +} + +fun Optional.Companion.presentIf(predicate: Boolean, value: V) = if (predicate) { + present(value) +} else { + absent() +} + +fun Optional.Companion.presentIfNot(predicate: Boolean, value: V) = if (predicate) { + absent() +} else { + present(value) +} + +fun Optional.Companion.presentAsList(value: V) = present(listOf(value)) \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt index 52a8f75..2bd02c2 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt @@ -1,6 +1,7 @@ package dev.datlag.aniflow.anilist.model import dev.datlag.aniflow.anilist.* +import dev.datlag.aniflow.anilist.PageMediaQuery import kotlinx.serialization.Serializable @Serializable @@ -103,7 +104,7 @@ data class Character( userPreferred = name.userPreferred?.ifBlank { null } ) - constructor(name: TrendingQuery.Name) : this( + constructor(name: PageMediaQuery.Name) : this( first = name.first?.ifBlank { null }, middle = name.middle?.ifBlank { null }, last = name.last?.ifBlank { null }, @@ -121,15 +122,6 @@ data class Character( userPreferred = name.userPreferred?.ifBlank { null } ) - constructor(name: SeasonQuery.Name) : this( - first = name.first?.ifBlank { null }, - middle = name.middle?.ifBlank { null }, - last = name.last?.ifBlank { null }, - full = name.full?.ifBlank { null }, - native = name.native?.ifBlank { null }, - userPreferred = name.userPreferred?.ifBlank { null } - ) - constructor(name: ListQuery.Name) : this( first = name.first?.ifBlank { null }, middle = name.middle?.ifBlank { null }, @@ -173,7 +165,7 @@ data class Character( medium = image.medium?.ifBlank { null }, ) - constructor(image: TrendingQuery.Image) : this( + constructor(image: PageMediaQuery.Image) : this( large = image.large?.ifBlank { null }, medium = image.medium?.ifBlank { null }, ) @@ -183,11 +175,6 @@ data class Character( medium = image.medium?.ifBlank { null } ) - constructor(image: SeasonQuery.Image) : this( - large = image.large?.ifBlank { null }, - medium = image.medium?.ifBlank { null } - ) - constructor(image: ListQuery.Image) : this( large = image.large?.ifBlank { null }, medium = image.medium?.ifBlank { null }, @@ -261,7 +248,7 @@ data class Character( ) } - operator fun invoke(birth: TrendingQuery.DateOfBirth): BirthDate? { + operator fun invoke(birth: PageMediaQuery.DateOfBirth): BirthDate? { if (birth.day == null && birth.month == null && birth.year == null) { return null } @@ -297,18 +284,6 @@ data class Character( ) } - operator fun invoke(birth: SeasonQuery.DateOfBirth): BirthDate? { - if (birth.day == null && birth.month == null && birth.year == null) { - return null - } - - return BirthDate( - day = birth.day, - month = birth.month, - year = birth.year - ) - } - operator fun invoke(birth: ListQuery.DateOfBirth): BirthDate? { if (birth.day == null && birth.month == null && birth.year == null) { return null @@ -365,7 +340,7 @@ data class Character( ) } - operator fun invoke(character: TrendingQuery.Node) : Character? { + operator fun invoke(character: PageMediaQuery.Node) : Character? { val name = character.name?.let(::Name) ?: return null val image = character.image?.let(::Image) ?: return null @@ -416,23 +391,6 @@ data class Character( ) } - operator fun invoke(character: SeasonQuery.Node) : Character? { - val name = character.name?.let(::Name) ?: return null - val image = character.image?.let(::Image) ?: return null - - return Character( - id = character.id, - name = name, - image = image, - gender = character.gender?.ifBlank { null }, - bloodType = character.bloodType?.ifBlank { null }, - birthDate = character.dateOfBirth?.let { BirthDate(it) }, - description = character.description?.ifBlank { null }, - isFavorite = character.isFavourite, - isFavoriteBlocked = character.isFavouriteBlocked - ) - } - operator fun invoke(character: ListQuery.Node) : Character? { val name = character.name?.let(::Name) ?: return null val image = character.image?.let(::Image) ?: return null diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt index b51a009..8de9333 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt @@ -2,6 +2,7 @@ package dev.datlag.aniflow.anilist.model import dev.datlag.aniflow.anilist.* import dev.datlag.aniflow.anilist.AdultContent +import dev.datlag.aniflow.anilist.PageMediaQuery import dev.datlag.aniflow.anilist.common.lastMonth import dev.datlag.aniflow.anilist.common.toLocalDate import dev.datlag.aniflow.anilist.type.* @@ -53,7 +54,7 @@ data class Medium( val volumes: Int = -1, val startDate: LocalDate? = null ) { - constructor(trending: TrendingQuery.Medium) : this( + constructor(trending: PageMediaQuery.Medium) : this( id = trending.id, idMal = trending.idMal, type = trending.type ?: MediaType.UNKNOWN__, @@ -157,58 +158,6 @@ data class Medium( startDate = airing.startDate?.toLocalDate() ) - constructor(season: SeasonQuery.Medium) : this( - id = season.id, - idMal = season.idMal, - type = season.type ?: MediaType.UNKNOWN__, - status = season.status ?: MediaStatus.UNKNOWN__, - description = season.description?.ifBlank { null }, - _episodes = season.episodes ?: -1, - avgEpisodeDurationInMin = season.duration ?: -1, - format = season.format ?: MediaFormat.UNKNOWN__, - _isAdult = season.isAdult ?: false, - genres = season.genresFilterNotNull()?.toSet() ?: emptySet(), - countryOfOrigin = season.countryOfOrigin?.toString()?.ifBlank { null }, - averageScore = season.averageScore ?: -1, - title = Title( - english = season.title?.english?.ifBlank { null }, - native = season.title?.native?.ifBlank { null }, - romaji = season.title?.romaji?.ifBlank { null }, - userPreferred = season.title?.userPreferred?.ifBlank { null } - ), - bannerImage = season.bannerImage?.ifBlank { null }, - coverImage = CoverImage( - color = season.coverImage?.color?.ifBlank { null }, - medium = season.coverImage?.medium?.ifBlank { null }, - large = season.coverImage?.large?.ifBlank { null }, - extraLarge = season.coverImage?.extraLarge?.ifBlank { null } - ), - nextAiringEpisode = season.nextAiringEpisode?.let(::NextAiring), - ranking = season.rankingsFilterNotNull()?.map(::Ranking)?.toSet() ?: emptySet(), - _characters = season.characters?.nodesFilterNotNull()?.mapNotNull(Character::invoke)?.toSet() ?: emptySet(), - entry = season.mediaListEntry?.let(::Entry), - trailer = season.trailer?.let { - val site = it.site?.ifBlank { null } - val thumbnail = it.thumbnail?.ifBlank { null } - - if (site == null || thumbnail == null) { - null - } else { - Trailer( - id = it.id?.ifBlank { null }, - site = site, - thumbnail = thumbnail, - ) - } - }, - isFavorite = season.isFavourite, - _isFavoriteBlocked = season.isFavouriteBlocked, - siteUrl = season.siteUrl?.ifBlank { null } ?: "$SITE_URL${season.id}", - chapters = season.chapters ?: -1, - volumes = season.volumes ?: -1, - startDate = season.startDate?.toLocalDate() - ) - constructor(query: MediumQuery.Media) : this( id = query.id, idMal = query.idMal, @@ -528,7 +477,7 @@ data class Medium( type = ranking.type ) - constructor(ranking: TrendingQuery.Ranking) : this( + constructor(ranking: PageMediaQuery.Ranking) : this( rank = ranking.rank, allTime = ranking.allTime ?: (ranking.season?.lastMonth() == null && ranking.year == null), year = ranking.year ?: -1, @@ -544,14 +493,6 @@ data class Medium( type = ranking.type ) - constructor(ranking: SeasonQuery.Ranking) : this( - rank = ranking.rank, - allTime = ranking.allTime ?: (ranking.season?.lastMonth() == null && ranking.year == null), - year = ranking.year ?: -1, - season = ranking.season?.lastMonth(), - type = ranking.type - ) - constructor(ranking: ListQuery.Ranking) : this( rank = ranking.rank, allTime = ranking.allTime ?: (ranking.season?.lastMonth() == null && ranking.year == null), @@ -593,7 +534,7 @@ data class Medium( startDate = entry.startedAt?.toLocalDate() ) - constructor(entry: TrendingQuery.MediaListEntry) : this( + constructor(entry: PageMediaQuery.MediaListEntry) : this( score = entry.score, status = entry.status ?: MediaListStatus.UNKNOWN__, progress = entry.progress, @@ -609,14 +550,6 @@ data class Medium( startDate = entry.startedAt?.toLocalDate() ) - constructor(entry: SeasonQuery.MediaListEntry) : this( - score = entry.score, - status = entry.status ?: MediaListStatus.UNKNOWN__, - progress = entry.progress, - repeatCount = entry.repeat, - startDate = entry.startedAt?.toLocalDate() - ) - constructor(entry: ListQuery.MediaList) : this( score = entry.score, status = entry.status ?: MediaListStatus.UNKNOWN__, @@ -706,7 +639,7 @@ data class Medium( */ val airingAt: Int ) { - constructor(nextAiringEpisode: TrendingQuery.NextAiringEpisode) : this( + constructor(nextAiringEpisode: PageMediaQuery.NextAiringEpisode) : this( episodes = nextAiringEpisode.episode, airingAt = nextAiringEpisode.airingAt ) @@ -721,11 +654,6 @@ data class Medium( airingAt = nextAiringEpisode.airingAt ) - constructor(nextAiringEpisode: SeasonQuery.NextAiringEpisode) : this( - episodes = nextAiringEpisode.episode, - airingAt = nextAiringEpisode.airingAt - ) - constructor(nextAiringEpisode: ListQuery.NextAiringEpisode) : this( episodes = nextAiringEpisode.episode, airingAt = nextAiringEpisode.airingAt diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/PageMediaQuery.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/PageMediaQuery.kt new file mode 100644 index 0000000..fe34b1d --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/PageMediaQuery.kt @@ -0,0 +1,81 @@ +package dev.datlag.aniflow.anilist.model + +import com.apollographql.apollo3.api.Optional +import dev.datlag.aniflow.anilist.AdultContent +import dev.datlag.aniflow.anilist.common.nextSeason +import dev.datlag.aniflow.anilist.common.presentAsList +import dev.datlag.aniflow.anilist.common.presentIfNot +import dev.datlag.aniflow.anilist.common.presentMediaSeason +import dev.datlag.aniflow.anilist.common.presentMediaType +import dev.datlag.aniflow.anilist.type.MediaSeason +import dev.datlag.aniflow.anilist.type.MediaSort +import dev.datlag.aniflow.anilist.type.MediaType +import kotlinx.datetime.Clock +import dev.datlag.aniflow.anilist.PageMediaQuery as PageMediaGraphQL + +sealed interface PageMediaQuery { + fun toGraphQL(): PageMediaGraphQL + + data class Trending( + val type: MediaType, + val nsfw: Boolean + ) : PageMediaQuery { + override fun toGraphQL() = PageMediaGraphQL( + perPage = Optional.present(20), + adultContent = Optional.presentIfNot(nsfw), + type = Optional.presentMediaType(type), + sort = Optional.presentAsList(MediaSort.TRENDING_DESC), + preventGenres = Optional.presentIfNot(nsfw, AdultContent.Genre.allTags), + statusVersion = 2, + html = true + ) + } + + data class PopularSeason( + val type: MediaType, + val nsfw: Boolean + ) : PageMediaQuery { + override fun toGraphQL() = PageMediaGraphQL( + perPage = Optional.present(20), + adultContent = Optional.presentIfNot(nsfw), + type = Optional.presentMediaType(type), + sort = Optional.presentAsList(MediaSort.POPULARITY_DESC), + preventGenres = Optional.presentIfNot(nsfw, AdultContent.Genre.allTags), + year = Optional.absent(), + season = Optional.absent(), + statusVersion = 2, + html = true + ) + } + + data class PopularNextSeason( + val type: MediaType, + val nsfw: Boolean, + val season: MediaSeason, + val year: Int + ) : PageMediaQuery { + + constructor( + type: MediaType, + nsfw: Boolean, + nextSeason: Pair = Clock.System.now().nextSeason + ) : this( + type = type, + nsfw = nsfw, + season = nextSeason.first, + year = nextSeason.second + ) + + override fun toGraphQL() = PageMediaGraphQL( + perPage = Optional.present(20), + adultContent = Optional.presentIfNot(nsfw), + type = Optional.presentMediaType(type), + sort = Optional.presentAsList(MediaSort.POPULARITY_DESC), + preventGenres = Optional.presentIfNot(nsfw, AdultContent.Genre.allTags), + year = Optional.present(year), + season = Optional.presentMediaSeason(season), + statusVersion = 2, + html = true + ) + } +} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/CollectionState.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/CollectionState.kt index 4bcb51a..a27229d 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/CollectionState.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/CollectionState.kt @@ -1,8 +1,6 @@ package dev.datlag.aniflow.anilist.state import dev.datlag.aniflow.anilist.SearchQuery -import dev.datlag.aniflow.anilist.SeasonQuery -import dev.datlag.aniflow.anilist.TrendingQuery import dev.datlag.aniflow.anilist.model.Medium import kotlinx.serialization.Serializable @@ -27,25 +25,6 @@ sealed interface CollectionState { data object Error : CollectionState companion object { - fun fromTrendingGraphQL(data: TrendingQuery.Data?): CollectionState { - val mediaList = data?.Page?.mediaFilterNotNull() - - if (mediaList.isNullOrEmpty()) { - return Error - } - - return Success(mediaList.map { Medium(it) }) - } - - fun fromSeasonGraphQL(data: SeasonQuery.Data?): CollectionState { - val mediaList = data?.Page?.mediaFilterNotNull() - - if (mediaList.isNullOrEmpty()) { - return Error - } - - return Success(mediaList.map { Medium(it) }) - } fun fromSearchGraphQL(data: SearchQuery.Data?): CollectionState { val mediaList = data?.Page?.mediaFilterNotNull() diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/HomeDefaultState.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/HomeDefaultState.kt new file mode 100644 index 0000000..1fed7eb --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/HomeDefaultState.kt @@ -0,0 +1,59 @@ +package dev.datlag.aniflow.anilist.state + +import com.apollographql.apollo3.api.ApolloResponse +import dev.datlag.aniflow.anilist.model.Medium +import dev.datlag.aniflow.anilist.model.PageMediaQuery +import dev.datlag.aniflow.anilist.PageMediaQuery as PageMediaGraphQL + +sealed interface HomeDefaultState { + + val isLoading: Boolean + get() = this !is PostLoading + + val isError: Boolean + get() = this is Error + + data object None : HomeDefaultState + + data class Loading( + internal val query: PageMediaQuery, + internal val fallback: Boolean + ) : HomeDefaultState { + + fun fromGraphQL(response: ApolloResponse): HomeDefaultState { + val data = response.data + + return if (data == null) { + if (fallback) { + Error(throwable = response.exception) + } else { + copy(fallback = true) + } + } else { + val mediaList = data.Page?.mediaFilterNotNull() + + if (mediaList.isNullOrEmpty()) { + if (fallback) { + Error(throwable = response.exception) + } else { + copy(fallback = true) + } + } else { + Success(mediaList.map(::Medium)) + } + } + } + } + + private sealed interface PostLoading : HomeDefaultState + + data class Success( + val collection: Collection + ) : PostLoading + + data class Error( + internal val throwable: Throwable? + ) : PostLoading +} + +sealed interface HomeDefaultAction { } \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.android.kt index 4845c54..84b7a64 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.android.kt @@ -1,9 +1,7 @@ package dev.datlag.aniflow.common -import dev.datlag.aniflow.anilist.TrendingQuery import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.model.Character -import dev.datlag.aniflow.settings.model.AppSettings import java.util.Locale import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle import dev.datlag.aniflow.settings.model.CharLanguage as SettingsChar 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 f874fbe..a6f8ea7 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -125,16 +125,6 @@ data object NetworkModule { baseUrl("https://api.nekosapi.com/v3/") }.create() } - bindSingleton { - val appSettings = instance() - - TrendingRepository( - apolloClient = instance(Constants.AniList.APOLLO_CLIENT), - fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), - nsfw = appSettings.adultContent, - viewManga = appSettings.viewManga - ) - } bindSingleton { val appSettings = instance() @@ -144,25 +134,6 @@ data object NetworkModule { nsfw = appSettings.adultContent ) } - bindSingleton { - val appSettings = instance() - - PopularSeasonRepository( - apolloClient = instance(Constants.AniList.APOLLO_CLIENT), - fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), - nsfw = appSettings.adultContent, - viewManga = appSettings.viewManga - ) - } - bindSingleton { - val appSettings = instance() - - PopularNextSeasonRepository( - apolloClient = instance(Constants.AniList.APOLLO_CLIENT), - fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), - nsfw = appSettings.adultContent - ) - } bindSingleton { CharacterRepository( client = instance(Constants.AniList.APOLLO_CLIENT).newBuilder().fetchPolicy(FetchPolicy.NetworkFirst).build(), @@ -223,5 +194,39 @@ data object NetworkModule { viewManga = appSettings.viewManga ) } + + bindProvider { + val appSettings = instance() + + TrendingStateMachine( + client = instance(Constants.AniList.APOLLO_CLIENT), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), + nsfw = appSettings.adultContent, + viewManga = appSettings.viewManga, + crashlytics = nullableFirebaseInstance()?.crashlytics + ) + } + bindProvider { + val appSettings = instance() + + PopularSeasonStateMachine( + client = instance(Constants.AniList.APOLLO_CLIENT), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), + nsfw = appSettings.adultContent, + viewManga = appSettings.viewManga, + crashlytics = nullableFirebaseInstance()?.crashlytics + ) + } + bindProvider { + val appSettings = instance() + + PopularNextSeasonStateMachine( + client = instance(Constants.AniList.APOLLO_CLIENT), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), + nsfw = appSettings.adultContent, + viewManga = appSettings.viewManga, + crashlytics = nullableFirebaseInstance()?.crashlytics + ) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt index 345637d..8e77731 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt @@ -5,6 +5,8 @@ import com.mayakapps.kache.InMemoryKache import com.mayakapps.kache.KacheStrategy import dev.datlag.aniflow.anilist.* import dev.datlag.aniflow.anilist.state.CollectionState +import dev.datlag.aniflow.anilist.state.HomeDefaultAction +import dev.datlag.aniflow.anilist.state.HomeDefaultState import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.tooling.async.scopeCatching import dev.datlag.tooling.async.suspendCatching @@ -73,17 +75,17 @@ data object StateSaver { return state } - fun updateTrending(state: CollectionState): CollectionState { + fun updateTrending(state: HomeDefaultState): HomeDefaultState { trendingLoading.update { false } return state } - fun updatePopularCurrent(state: CollectionState): CollectionState { + fun updatePopularCurrent(state: HomeDefaultState): HomeDefaultState { popularCurrentLoading.update { false } return state } - fun updatePopularNext(state: CollectionState): CollectionState { + fun updatePopularNext(state: HomeDefaultState): HomeDefaultState { popularNextLoading.update { false } return state } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt index 681dcd5..de325bf 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt @@ -3,16 +3,17 @@ package dev.datlag.aniflow.ui.navigation.screen.home import com.arkivanov.decompose.router.slot.ChildSlot import com.arkivanov.decompose.value.Value import dev.datlag.aniflow.anilist.AiringTodayRepository -import dev.datlag.aniflow.anilist.TrendingRepository import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.model.User import dev.datlag.aniflow.anilist.state.CollectionState +import dev.datlag.aniflow.anilist.state.HomeDefaultState import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.aniflow.settings.model.TitleLanguage import dev.datlag.aniflow.trace.TraceRepository import dev.datlag.aniflow.ui.navigation.Component import dev.datlag.aniflow.ui.navigation.DialogComponent import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow interface HomeComponent : Component { val viewing: Flow @@ -21,9 +22,9 @@ interface HomeComponent : Component { val titleLanguage: Flow val airing: Flow - val trending: Flow - val popularNow: Flow - val popularNext: Flow + val trending: StateFlow + val popularNow: StateFlow + val popularNext: StateFlow val traceState: Flow diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt index 61bd271..9372310 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt @@ -9,12 +9,13 @@ import com.arkivanov.decompose.value.Value import dev.chrisbanes.haze.HazeState import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.anilist.AiringTodayRepository -import dev.datlag.aniflow.anilist.PopularNextSeasonRepository -import dev.datlag.aniflow.anilist.PopularSeasonRepository -import dev.datlag.aniflow.anilist.TrendingRepository +import dev.datlag.aniflow.anilist.PopularNextSeasonStateMachine +import dev.datlag.aniflow.anilist.PopularSeasonStateMachine +import dev.datlag.aniflow.anilist.TrendingStateMachine import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.model.User import dev.datlag.aniflow.anilist.state.CollectionState +import dev.datlag.aniflow.anilist.state.HomeDefaultState import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.aniflow.common.onRender import dev.datlag.aniflow.model.coroutines.Executor @@ -70,35 +71,37 @@ class HomeScreenComponent( initialValue = AiringTodayRepository.State.None ) - private val trendingRepository by instance() - override val trending: MutableStateFlow = trendingRepository.trending.map { + private val trendingRepository by instance() + override val trending: StateFlow = trendingRepository.state.map { StateSaver.Home.updateTrending(it) }.flowOn( context = ioDispatcher() - ).mutableStateIn( + ).stateIn( scope = stateScope, - initialValue = CollectionState.None + started = SharingStarted.WhileSubscribed(), + initialValue = trendingRepository.currentState ) - private val popularSeasonRepository by instance() - override val popularNow: MutableStateFlow = popularSeasonRepository.popularThisSeason.map { + private val popularSeasonRepository by instance() + override val popularNow: StateFlow = popularSeasonRepository.state.map { StateSaver.Home.updatePopularCurrent(it) }.flowOn( context = ioDispatcher() - ).mutableStateIn( + ).stateIn( scope = stateScope, - initialValue = CollectionState.None + started = SharingStarted.WhileSubscribed(), + initialValue = popularSeasonRepository.currentState ) - private val popularNextSeasonRepository by instance() - override val popularNext: Flow = popularNextSeasonRepository.popularNextSeason.map { + private val popularNextSeasonRepository by instance() + override val popularNext: StateFlow = popularNextSeasonRepository.state.map { StateSaver.Home.updatePopularNext(it) }.flowOn( context = ioDispatcher() ).stateIn( scope = stateScope, started = SharingStarted.WhileSubscribed(), - initialValue = CollectionState.None + initialValue = popularNextSeasonRepository.currentState ) private val traceRepository by instance() @@ -152,8 +155,6 @@ class HomeScreenComponent( StateSaver.Home.updateAllLoading() launchIO { viewTypeExecutor.enqueue { - clearForTypeChange() - appSettings.setViewManga(false) } } @@ -163,8 +164,6 @@ class HomeScreenComponent( StateSaver.Home.updateAllLoading() launchIO { viewTypeExecutor.enqueue { - clearForTypeChange() - appSettings.setViewManga(true) } } @@ -189,9 +188,4 @@ class HomeScreenComponent( override fun clearTrace() { traceRepository.clear() } - - private suspend fun clearForTypeChange() { - trending.emit(CollectionState.None) - popularNow.emit(CollectionState.None) - } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt index 693d4a0..5a61672 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt @@ -17,17 +17,18 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.state.CollectionState +import dev.datlag.aniflow.anilist.state.HomeDefaultState import dev.datlag.aniflow.settings.model.TitleLanguage import dev.datlag.aniflow.ui.custom.ErrorContent import dev.datlag.aniflow.ui.navigation.screen.home.component.default.MediumCard import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow -@OptIn(ExperimentalFoundationApi::class) @Composable fun DefaultOverview( title: String, - flow: Flow, + flow: StateFlow, titleLanguage: TitleLanguage?, onMediumClick: (Medium) -> Unit, ) { @@ -35,7 +36,7 @@ fun DefaultOverview( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - val state by flow.collectAsStateWithLifecycle(CollectionState.None) + val state by flow.collectAsStateWithLifecycle() Row( modifier = Modifier.padding(start = 16.dp, end = 4.dp).fillMaxWidth(), @@ -50,10 +51,10 @@ fun DefaultOverview( } when (val current = state) { - is CollectionState.None -> { + is HomeDefaultState.None, is HomeDefaultState.Loading -> { Loading() } - is CollectionState.Success -> { + is HomeDefaultState.Success -> { LazyRow( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -72,7 +73,7 @@ fun DefaultOverview( } } } - is CollectionState.Error -> { + is HomeDefaultState.Error -> { ErrorContent( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), horizontal = true