From 6e0b77c393cd6cabe967cd746402cc661c8b65cc Mon Sep 17 00:00:00 2001 From: DatLag Date: Thu, 2 May 2024 21:19:26 +0200 Subject: [PATCH] switch airing and trending to easier repository --- .../datlag/aniflow/anilist/AdultContent.kt | 7 + .../aniflow/anilist/AiringTodayRepository.kt | 84 ++++++++++ .../anilist/AiringTodayStateMachine.kt | 152 ------------------ .../dev/datlag/aniflow/anilist/StateSaver.kt | 2 - .../anilist/TrendingAnimeStateMachine.kt | 133 --------------- .../aniflow/anilist/TrendingRepository.kt | 105 ++++++++++++ .../datlag/aniflow/module/NetworkModule.kt | 30 ++-- .../dev/datlag/aniflow/other/StateSaver.kt | 35 ++-- .../screen/initial/home/HomeComponent.kt | 9 +- .../initial/home/HomeScreenComponent.kt | 21 +-- .../initial/home/component/AiringOverview.kt | 17 +- .../home/component/TrendingOverview.kt | 22 ++- .../navigation/screen/medium/MediumScreen.kt | 2 +- .../screen/medium/MediumScreenComponent.kt | 26 ++- .../screen/medium/component/RatingSection.kt | 6 +- .../datlag/aniflow/ui/theme/SchemeTheme.kt | 18 ++- .../dev/datlag/aniflow/model/ExtendAny.kt | 4 + .../dev/datlag/aniflow/model/ExtendFlow.kt | 6 + 18 files changed, 296 insertions(+), 383 deletions(-) create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt delete mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt delete mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AdultContent.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AdultContent.kt index 8c02a6f..a47c44a 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AdultContent.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AdultContent.kt @@ -1,6 +1,13 @@ package dev.datlag.aniflow.anilist internal data object AdultContent { + + fun isAdultContent(schedule: AiringQuery.AiringSchedule): Boolean { + return schedule.media?.isAdult == true || schedule.media?.genresFilterNotNull()?.any { + Genre.exists(it) + } == true + } + sealed interface Genre : CharSequence { val tag: String diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt new file mode 100644 index 0000000..ca2728c --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt @@ -0,0 +1,84 @@ +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.type.AiringSort +import kotlinx.coroutines.flow.* +import kotlinx.datetime.Clock +import kotlin.time.Duration.Companion.hours + +class AiringTodayRepository( + private val apolloClient: ApolloClient, + private val nsfw: Flow = flowOf(false), +) { + + private val page = MutableStateFlow(0) + private val query = combine(page, nsfw.distinctUntilChanged()) { p, n -> + Query( + page = p, + nsfw = n, + ) + } + + private val airingPreFilter = query.transform { + return@transform emitAll(apolloClient.query(it.toGraphQL()).toFlow()) + } + val airing = combine(airingPreFilter, nsfw) { q, n -> + State.fromGraphQL(q.data, n) + } + + fun nextPage() = page.getAndUpdate { + it + 1 + } + + fun previousPage() = page.getAndUpdate { + it - 1 + } + + private data class Query( + val page: Int, + val nsfw: Boolean + ) { + fun toGraphQL() = AiringQuery( + page = Optional.present(page), + perPage = Optional.present(20), + sort = Optional.present(listOf(AiringSort.TIME)), + airingAtGreater = Optional.present( + Clock.System.now().minus(1.hours).epochSeconds.toInt() + ), + statusVersion = Optional.present(2), + html = Optional.present(true) + ) + } + + sealed interface State { + data class Success( + val collection: Collection + ) : State + + data object Error : State + + companion object { + fun fromGraphQL(data: AiringQuery.Data?, nsfw: Boolean): State { + val airingList = data?.Page?.airingSchedulesFilterNotNull()?.mapNotNull { + if (nsfw) { + it + } else { + if (AdultContent.isAdultContent(it)) { + null + } else { + it + } + } + } + + if (airingList.isNullOrEmpty()) { + return Error + } + + return Success(airingList) + } + } + } +} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt deleted file mode 100644 index 077135a..0000000 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt +++ /dev/null @@ -1,152 +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.state.CommonState -import dev.datlag.aniflow.anilist.type.AiringSort -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 dev.datlag.tooling.safeSubList -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.datetime.Clock -import kotlin.time.Duration.Companion.hours -import kotlin.time.Duration.Companion.seconds - -@OptIn(ExperimentalCoroutinesApi::class) -class AiringTodayStateMachine( - private val client: ApolloClient, - private val fallbackClient: ApolloClient, - private val crashlytics: FirebaseFactory.Crashlytics? -) : FlowReduxStateMachine( - initialState = currentState -) { - - init { - spec { - inState { - onEnterEffect { - currentState = it - } - onEnter { state -> - Cache.getAiring(state.snapshot.query)?.let { - return@onEnter state.override { State.Success(query, it) } - } - - val response = CatchResult.repeat(times = 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 { - val wantedContent = if (!state.snapshot.adultContent) { - val content = it.Page?.airingSchedulesFilterNotNull() ?: emptyList() - val filtered = content.filterNot { c -> - c.media?.isAdult == true - }.filterNot { c -> - val genres = c.media?.genresFilterNotNull() ?: emptyList() - - genres.any { g -> - AdultContent.Genre.exists(g) - } - } - filtered - } else { - it.Page?.airingSchedulesFilterNotNull() ?: emptyList() - } - - State.Success( - state.snapshot.query, - it.copy( - Page = it.Page?.copy( - airingSchedules = wantedContent.safeSubList(0, 10) - ) - ) - ) - } - - state.override { - response.asSuccess { - crashlytics?.log(it) - - State.Error(query, adultContent) - } - } - } - } - inState { - onEnterEffect { - Cache.setAiring(it.query, it.data) - - currentState = it - } - } - inState { - onEnterEffect { - currentState = it - } - on { _, state -> - state.override { - State.Loading(state.snapshot.query, state.snapshot.adultContent) - } - } - } - } - } - - sealed interface State : CommonState { - - override val isLoadingOrWaiting: Boolean - get() = this is Loading - - data class Loading( - internal val query: AiringQuery, - val adultContent: Boolean = false - ) : State { - constructor( - page: Int, - perPage: Int = 20, - adultContent: Boolean = false - ) : this( - query = AiringQuery( - page = Optional.present(page), - perPage = Optional.present(perPage), - sort = Optional.present(listOf(AiringSort.TIME)), - airingAtGreater = Optional.present( - Clock.System.now().minus(1.hours).epochSeconds.toInt() - ), - statusVersion = Optional.present(2), - html = Optional.present(true) - ), - adultContent = adultContent - ) - } - - data class Success( - internal val query: AiringQuery, - val data: AiringQuery.Data - ) : State - - data class Error( - internal val query: AiringQuery, - val adultContent: Boolean = false - ) : State - } - - sealed interface Action { - data object Retry : Action - } - - companion object { - var currentState: State - get() = StateSaver.airing - set(value) { - StateSaver.airing = 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 index 70cea9c..66af5d2 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt @@ -5,8 +5,6 @@ import dev.datlag.aniflow.anilist.state.SeasonState import kotlinx.datetime.Clock internal object StateSaver { - var trendingAnime: TrendingAnimeStateMachine.State = TrendingAnimeStateMachine.State.Loading(page = 0) - var airing: AiringTodayStateMachine.State = AiringTodayStateMachine.State.Loading(page = 0) var popularSeason: SeasonState = SeasonState.Loading(page = 0) var popularNextSeason: SeasonState = run { val (nextSeason, nextYear) = Clock.System.now().nextSeason diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt deleted file mode 100644 index 772126a..0000000 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt +++ /dev/null @@ -1,133 +0,0 @@ -package dev.datlag.aniflow.anilist - -import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.annotations.ApolloExperimental -import com.apollographql.apollo3.api.Optional -import com.freeletics.flowredux.dsl.FlowReduxStateMachine -import dev.datlag.aniflow.anilist.state.CommonState -import dev.datlag.aniflow.anilist.type.MediaSort -import dev.datlag.aniflow.anilist.type.MediaType -import dev.datlag.aniflow.firebase.FirebaseFactory -import dev.datlag.aniflow.model.* -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlin.time.Duration.Companion.seconds - -@OptIn(ExperimentalCoroutinesApi::class, ApolloExperimental::class) -class TrendingAnimeStateMachine( - private val client: ApolloClient, - private val fallbackClient: ApolloClient, - private val crashlytics: FirebaseFactory.Crashlytics? -) : FlowReduxStateMachine( - initialState = currentState -) { - - init { - spec { - inState { - onEnterEffect { - currentState = it - } - onEnter { state -> - Cache.getTrending(state.snapshot.query)?.let { - return@onEnter state.override { State.Success(query, it) } - } - - 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 { - State.Success(state.snapshot.query, it) - } - - state.override { - response.asSuccess { - crashlytics?.log(it) - - State.Error(query) - } - } - } - } - inState { - onEnterEffect { - Cache.setTrending(it.query, it.data) - currentState = it - } - } - inState { - onEnterEffect { - currentState = it - } - on { _, state -> - state.override { - State.Loading( - state.snapshot.query - ) - } - } - } - } - } - - sealed interface State : CommonState { - - override val isLoadingOrWaiting: Boolean - get() = this is Loading - - data class Loading( - internal val query: TrendingQuery - ) : State { - constructor( - page: Int, - perPage: Int = 10, - adultContent: Boolean = false, - type: MediaType = MediaType.ANIME - ) : this( - TrendingQuery( - page = Optional.present(page), - perPage = Optional.present(perPage), - adultContent = if (!adultContent) { - Optional.present(adultContent) - } else { - Optional.absent() - }, - type = Optional.present(type), - sort = Optional.present(listOf(MediaSort.TRENDING_DESC)), - preventGenres = if (!adultContent) { - Optional.present(AdultContent.Genre.allTags) - } else { - Optional.absent() - }, - statusVersion = Optional.present(2), - html = Optional.present(true) - ) - ) - } - - data class Success( - internal val query: TrendingQuery, - val data: TrendingQuery.Data - ) : State - - data class Error( - internal val query: TrendingQuery - ) : State - } - - sealed interface Action { - data object Retry : Action - } - - companion object { - var currentState: TrendingAnimeStateMachine.State - get() = StateSaver.trendingAnime - set(value) { - StateSaver.trendingAnime = value - } - } -} \ 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 new file mode 100644 index 0000000..74144b2 --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt @@ -0,0 +1,105 @@ +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.type.MediaSort +import dev.datlag.aniflow.anilist.type.MediaType +import kotlinx.coroutines.flow.* + +class TrendingRepository( + private val apolloClient: ApolloClient, + private val nsfw: Flow = flowOf(false), +) { + + private val page = MutableStateFlow(0) + private val type = MutableStateFlow(MediaType.ANIME) + private val query = combine(page, type, nsfw.distinctUntilChanged()) { p, t, n -> + Query( + page = p, + type = t, + nsfw = n + ) + } + + val trending = query.transform { + return@transform emitAll(apolloClient.query(it.toGraphQL()).toFlow()) + }.map { + State.fromGraphQL(it.data) + } + + fun nextPage() = page.getAndUpdate { + it + 1 + } + + fun previousPage() = page.getAndUpdate { + it - 1 + } + + fun viewAnime() { + type.getAndUpdate { + if (it == MediaType.ANIME) { + it + } else { + page.update { 0 } + MediaType.ANIME + } + } + } + + fun viewManga() { + type.getAndUpdate { + if (it == MediaType.MANGA) { + it + } else { + page.update { 0 } + MediaType.MANGA + } + } + } + + 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 = 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) + ) + } + + sealed interface State { + data class Success( + val collection: Collection + ) : State + + data object Error : State + + companion object { + fun fromGraphQL(data: TrendingQuery.Data?): State { + val mediaList = data?.Page?.mediaFilterNotNull() + + if (mediaList.isNullOrEmpty()) { + return Error + } + + return Success(mediaList.map { Medium(it) }) + } + } + } +} \ 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 41f4cb6..4578b09 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -89,20 +89,6 @@ data object NetworkModule { .serverUrl(Constants.AniList.SERVER_URL) .build() } - bindProvider { - TrendingAnimeStateMachine( - client = instance(Constants.AniList.APOLLO_CLIENT), - fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), - crashlytics = nullableFirebaseInstance()?.crashlytics - ) - } - bindProvider { - AiringTodayStateMachine( - client = instance(Constants.AniList.APOLLO_CLIENT), - fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), - crashlytics = nullableFirebaseInstance()?.crashlytics - ) - } bindProvider { PopularSeasonStateMachine( client = instance(Constants.AniList.APOLLO_CLIENT), @@ -142,5 +128,21 @@ data object NetworkModule { crashlytics = nullableFirebaseInstance()?.crashlytics ) } + bindSingleton { + val appSettings = instance() + + TrendingRepository( + apolloClient = instance(Constants.AniList.APOLLO_CLIENT), + nsfw = appSettings.adultContent + ) + } + bindSingleton { + val appSettings = instance() + + AiringTodayRepository( + apolloClient = instance(Constants.AniList.APOLLO_CLIENT), + nsfw = appSettings.adultContent + ) + } } } \ 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 1ff5b68..5d284ee 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt @@ -1,10 +1,7 @@ package dev.datlag.aniflow.other import androidx.compose.ui.graphics.Color -import dev.datlag.aniflow.anilist.AiringTodayStateMachine -import dev.datlag.aniflow.anilist.PopularNextSeasonStateMachine -import dev.datlag.aniflow.anilist.PopularSeasonStateMachine -import dev.datlag.aniflow.anilist.TrendingAnimeStateMachine +import dev.datlag.aniflow.anilist.* import dev.datlag.aniflow.anilist.state.SeasonState import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.tooling.compose.ioDispatcher @@ -45,17 +42,11 @@ data object StateSaver { } data object Home { - private val _airingState = MutableStateFlow(airingState) - private val _trendingState = MutableStateFlow(trendingState) + private val airingLoading = MutableStateFlow(true) + private val trendingLoading = MutableStateFlow(true) private val _popularCurrentState = MutableStateFlow(popularCurrentState) private val _popularNextState = MutableStateFlow(popularNextState) - val airingState: AiringTodayStateMachine.State - get() = AiringTodayStateMachine.currentState - - val trendingState: TrendingAnimeStateMachine.State - get() = TrendingAnimeStateMachine.currentState - val popularCurrentState: SeasonState get() = PopularSeasonStateMachine.currentState @@ -63,26 +54,28 @@ data object StateSaver { get() = PopularNextSeasonStateMachine.currentState val currentAllLoading: Boolean - get() = _airingState.value.isLoadingOrWaiting - && _trendingState.value.isLoadingOrWaiting + get() = airingLoading.value + && trendingLoading.value && _popularCurrentState.value.isLoadingOrWaiting && _popularNextState.value.isLoadingOrWaiting val isAllLoading = combine( - _airingState, - _trendingState, + airingLoading, + trendingLoading, _popularCurrentState, _popularNextState ) { t1, t2, t3, t4 -> - t1.isLoadingOrWaiting && t2.isLoadingOrWaiting && t3.isLoadingOrWaiting && t4.isLoadingOrWaiting + t1 && t2 && t3.isLoadingOrWaiting && t4.isLoadingOrWaiting }.flowOn(ioDispatcher()).distinctUntilChanged() - fun updateAiring(state: AiringTodayStateMachine.State) = _airingState.updateAndGet { - state + fun updateAiring(state: AiringTodayRepository.State): AiringTodayRepository.State { + airingLoading.update { false } + return state } - fun updateTrending(state: TrendingAnimeStateMachine.State) = _trendingState.updateAndGet { - state + fun updateTrending(state: TrendingRepository.State): TrendingRepository.State { + trendingLoading.update { false } + return state } fun updatePopularCurrent(state: SeasonState) = _popularCurrentState.updateAndGet { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt index 35f220e..89944bc 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt @@ -2,9 +2,8 @@ package dev.datlag.aniflow.ui.navigation.screen.initial.home import com.arkivanov.decompose.router.slot.ChildSlot import com.arkivanov.decompose.value.Value -import dev.datlag.aniflow.anilist.AiringTodayStateMachine -import dev.datlag.aniflow.anilist.PopularSeasonStateMachine -import dev.datlag.aniflow.anilist.TrendingAnimeStateMachine +import dev.datlag.aniflow.anilist.AiringTodayRepository +import dev.datlag.aniflow.anilist.TrendingRepository import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.state.SeasonState import dev.datlag.aniflow.settings.model.AppSettings @@ -20,8 +19,8 @@ import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle interface HomeComponent : ContentHolderComponent { val titleLanguage: Flow - val airingState: Flow - val trendingState: Flow + val airingState: Flow + val trendingState: Flow val popularSeasonState: Flow val popularNextSeasonState: Flow diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt index 5cbeab9..341d111 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt @@ -6,10 +6,7 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.router.slot.* import com.arkivanov.decompose.value.Value import dev.datlag.aniflow.LocalDI -import dev.datlag.aniflow.anilist.AiringTodayStateMachine -import dev.datlag.aniflow.anilist.PopularNextSeasonStateMachine -import dev.datlag.aniflow.anilist.PopularSeasonStateMachine -import dev.datlag.aniflow.anilist.TrendingAnimeStateMachine +import dev.datlag.aniflow.anilist.* import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.state.SeasonState import dev.datlag.aniflow.common.onRender @@ -39,19 +36,15 @@ class HomeScreenComponent( private val appSettings by di.instance() override val titleLanguage: Flow = appSettings.titleLanguage.flowOn(ioDispatcher()) - private val airingTodayStateMachine by di.instance() - override val airingState: Flow = airingTodayStateMachine.state.map { + private val airingTodayRepository by di.instance() + override val airingState: Flow = airingTodayRepository.airing.map { StateSaver.Home.updateAiring(it) - }.flowOn( - context = ioDispatcher() - ).distinctUntilChanged() + } - private val trendingAnimeStateMachine by di.instance() - override val trendingState: Flow = trendingAnimeStateMachine.state.map { + private val trendingRepository by di.instance() + override val trendingState: Flow = trendingRepository.trending.map { StateSaver.Home.updateTrending(it) - }.flowOn( - context = ioDispatcher() - ).distinctUntilChanged() + } private val popularSeasonStateMachine by di.instance() override val popularSeasonState: Flow = popularSeasonStateMachine.state.map { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringOverview.kt index a6d359d..2c34261 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringOverview.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringOverview.kt @@ -18,30 +18,29 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import dev.datlag.aniflow.anilist.AiringQuery -import dev.datlag.aniflow.anilist.AiringTodayStateMachine +import dev.datlag.aniflow.anilist.AiringTodayRepository import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import dev.datlag.tooling.safeSubList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle @Composable fun AiringOverview( - state: Flow, + state: Flow, titleLanguage: SettingsTitle?, onClick: (Medium) -> Unit ) { - val loadingState by state.collectAsStateWithLifecycle(StateSaver.Home.airingState) + val loadingState by state.collectAsStateWithLifecycle(null) when (val reachedState = loadingState) { - is AiringTodayStateMachine.State.Loading -> { - Loading() - } - is AiringTodayStateMachine.State.Success -> { + null -> Loading() + is AiringTodayRepository.State.Success -> { SuccessContent( - data = reachedState.data.Page?.airingSchedulesFilterNotNull() ?: emptyList(), + data = reachedState.collection.toList(), titleLanguage = titleLanguage, onClick = onClick ) @@ -83,7 +82,7 @@ private fun SuccessContent( flingBehavior = rememberSnapFlingBehavior(state), contentPadding = PaddingValues(horizontal = 16.dp) ) { - items(data, key = { it.episode to it.media?.id }) { media -> + items(data.safeSubList(0, 10), key = { it.episode to it.media?.id }) { media -> AiringCard( airing = media, titleLanguage = titleLanguage, diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/TrendingOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/TrendingOverview.kt index 3b15c84..0cab00c 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/TrendingOverview.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/TrendingOverview.kt @@ -13,12 +13,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp -import dev.datlag.aniflow.anilist.TrendingAnimeStateMachine -import dev.datlag.aniflow.anilist.TrendingQuery +import dev.datlag.aniflow.anilist.TrendingRepository import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import dev.datlag.tooling.safeSubList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -27,19 +27,17 @@ import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle @Composable fun TrendingOverview( - state: Flow, + state: Flow, titleLanguage: SettingsTitle?, onClick: (Medium) -> Unit, ) { - val loadingState by state.collectAsStateWithLifecycle(StateSaver.Home.trendingState) + val loadingState by state.collectAsStateWithLifecycle(null) when (val reachedState = loadingState) { - is TrendingAnimeStateMachine.State.Loading -> { - Loading() - } - is TrendingAnimeStateMachine.State.Success -> { + null -> Loading() + is TrendingRepository.State.Success -> { SuccessContent( - data = reachedState.data.Page?.mediaFilterNotNull() ?: emptyList(), + data = reachedState.collection.toList(), titleLanguage = titleLanguage, onClick = onClick ) @@ -65,7 +63,7 @@ private fun Loading() { @OptIn(ExperimentalFoundationApi::class) @Composable private fun SuccessContent( - data: List, + data: List, titleLanguage: SettingsTitle?, onClick: (Medium) -> Unit ) { @@ -80,9 +78,9 @@ private fun SuccessContent( horizontalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(horizontal = 16.dp) ) { - itemsIndexed(data, key = { _, it -> it.id }) { _, medium -> + itemsIndexed(data.safeSubList(0, 10), key = { _, it -> it.id }) { _, medium -> MediumCard( - medium = Medium(medium), + medium = medium, titleLanguage = titleLanguage, modifier = Modifier .width(200.dp) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt index 86705a5..8eed995 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt @@ -161,7 +161,7 @@ fun MediumScreen(component: MediumComponent) { ratedFlow = component.rated, popularFlow = component.popular, scoreFlow = component.score, - modifier = Modifier.fillParentMaxWidth().padding(16.dp) + modifier = Modifier.fillParentMaxWidth().padding(horizontal = 16.dp).padding(top = 16.dp) ) } item { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt index 34a2b24..8ab2a79 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt @@ -106,43 +106,39 @@ class MediumScreenComponent( override val genres: Flow> = mediumSuccessState.map { it.data.genres - } + }.mapNotEmpty() override val format: Flow = mediumSuccessState.map { it.data.format } - private val nextAiringEpisode: Flow = mediumSuccessState.map { - it.data.nextAiringEpisode - } - override val episodes: Flow = mediumSuccessState.map { it.data.episodes - } + }.distinctUntilChanged() override val duration: Flow = mediumSuccessState.map { it.data.avgEpisodeDurationInMin - } + }.distinctUntilChanged() override val status: Flow = mediumSuccessState.map { it.data.status - } + }.distinctUntilChanged() - override val rated: Flow = mediumSuccessState.map { + override val rated: Flow = mediumSuccessState.mapNotNull { it.data.rated() - } + }.distinctUntilChanged() - override val popular: Flow = mediumSuccessState.map { + override val popular: Flow = mediumSuccessState.mapNotNull { it.data.popular() - } + }.distinctUntilChanged() override val score: Flow = mediumSuccessState.mapNotNull { - it.data.averageScore.ifValue(-1) { return@mapNotNull null } - } + it.data.averageScore.asNullIf(-1) + }.distinctUntilChanged() override val characters: Flow> = mediumSuccessState.map { it.data.characters - } + }.mapNotEmpty() private val changedRating: MutableStateFlow = MutableStateFlow(initialMedium.entry?.score?.toInt() ?: -1) override val rating: Flow = combine( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/RatingSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/RatingSection.kt index 0ac2c3d..75a6da8 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/RatingSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/RatingSection.kt @@ -7,12 +7,16 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.common.popular import dev.datlag.aniflow.common.rated +import dev.datlag.aniflow.model.asNullIf +import dev.datlag.aniflow.model.ifValue +import dev.datlag.aniflow.model.ifValueOrNull import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.flow.Flow @@ -33,7 +37,7 @@ fun RatingSection( ) { val rated by ratedFlow.collectAsStateWithLifecycle(initialMedium.rated()) val popular by popularFlow.collectAsStateWithLifecycle(initialMedium.popular()) - val score by scoreFlow.collectAsStateWithLifecycle(initialMedium.averageScore) + val score by scoreFlow.collectAsStateWithLifecycle(initialMedium.averageScore.asNullIf(-1)) rated?.let { Column( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt index 93b380d..ae6c388 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt @@ -41,6 +41,10 @@ data object SchemeTheme { }.getOrNull() internal suspend fun getOrPut(key: Any, fallback: DominantColorState) = suspendCatching { + kache.getIfAvailable(key) + }.getOrNull() ?: suspendCatching { + kache.put(key, fallback) + }.getOrNull() ?: suspendCatching { kache.getOrPut(key) { fallback } }.getOrNull() @@ -114,22 +118,28 @@ fun rememberSchemeThemeDominantColorState( return null } - val fallbackState = remember(key) { + val existingState = remember(key) { SchemeTheme.get(key) - } ?: rememberPainterDominantColorState( + } ?: SchemeTheme.get(key) + + if (existingState != null) { + return existingState + } + + val fallbackState = rememberPainterDominantColorState( defaultColor = defaultColor, defaultOnColor = defaultOnColor, builder = builder, isSwatchValid = isSwatchValid, coroutineContext = ioDispatcher() ) - val state by produceState(fallbackState, key) { + val state by produceState?>(null, key) { value = withIOContext { SchemeTheme.getOrPut(key, fallbackState) ?: fallbackState } } - return state + return remember(state) { state ?: fallbackState } } @Composable diff --git a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/ExtendAny.kt b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/ExtendAny.kt index 64ebe93..7342d97 100644 --- a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/ExtendAny.kt +++ b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/ExtendAny.kt @@ -4,6 +4,10 @@ inline fun T.ifValue(value: T, defaultValue: () -> T): T { return if (this == value) defaultValue() else this } +inline fun T.asNullIf(value: T): T? { + return if (this == value) null else this +} + inline fun T?.ifValueOrNull(value: T, defaultValue: () -> T): T { return if (this == value || this == null) defaultValue() else this } diff --git a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/ExtendFlow.kt b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/ExtendFlow.kt index aad16a7..8112cf3 100644 --- a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/ExtendFlow.kt +++ b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/ExtendFlow.kt @@ -25,4 +25,10 @@ suspend fun FlowCollector.emitNotNull(value: T?) { if (value != null) { emit(value) } +} + +inline fun > Flow.mapNotEmpty(): Flow = transform { value -> + if (value.isNotEmpty()) { + return@transform emit(value) + } } \ No newline at end of file