diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt index 5966905..ff7bb23 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt @@ -7,6 +7,8 @@ import dev.datlag.aniflow.anilist.model.Medium 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.saveFirstOrNull import dev.datlag.tooling.async.suspendCatching import dev.datlag.tooling.safeSubList import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -19,6 +21,7 @@ import kotlin.time.Duration.Companion.hours @OptIn(ExperimentalCoroutinesApi::class) class AiringTodayStateMachine( private val client: ApolloClient, + private val fallbackClient: ApolloClient, private val crashlytics: FirebaseFactory.Crashlytics? ) : FlowReduxStateMachine( initialState = currentState @@ -35,8 +38,14 @@ class AiringTodayStateMachine( return@onEnter state.override { State.Success(query, it) } } - val response = CatchResult.result { - client.query(state.snapshot.query).execute().dataOrThrow() + val response = CatchResult.repeat(times = 2) { + val query = client.query(state.snapshot.query) + + query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + }.mapError { + val query = fallbackClient.query(state.snapshot.query) + + query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() }.mapSuccess { val wantedContent = if (!state.snapshot.adultContent) { val content = it.Page?.airingSchedulesFilterNotNull() ?: emptyList() @@ -68,15 +77,7 @@ class AiringTodayStateMachine( response.asSuccess { crashlytics?.log(it) - if (retry <= 3) { - State.Loading( - query, - adultContent, - retry + 1 - ) - } else { - State.Error(query, adultContent) - } + State.Error(query, adultContent) } } } @@ -104,8 +105,7 @@ class AiringTodayStateMachine( sealed interface State { data class Loading( internal val query: AiringQuery, - val adultContent: Boolean = false, - internal val retry: Int = 0 + val adultContent: Boolean = false ) : State { constructor( page: Int, diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt index dda4ece..639bef4 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt @@ -6,12 +6,15 @@ import com.freeletics.flowredux.dsl.FlowReduxStateMachine import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.firebase.FirebaseFactory import dev.datlag.aniflow.model.CatchResult +import dev.datlag.aniflow.model.mapError +import dev.datlag.aniflow.model.saveFirstOrNull import dev.datlag.tooling.async.suspendCatching import kotlinx.coroutines.ExperimentalCoroutinesApi @OptIn(ExperimentalCoroutinesApi::class) class MediumStateMachine( private val client: ApolloClient, + private val fallbackClient: ApolloClient, private val crashlytics: FirebaseFactory.Crashlytics?, private val id: Int ) : FlowReduxStateMachine( @@ -28,8 +31,14 @@ class MediumStateMachine( currentState = it } onEnter { state -> - val response = CatchResult.result { - client.query(state.snapshot.query).execute().dataOrThrow() + val response = CatchResult.repeat(times = 2) { + val query = client.query(state.snapshot.query) + + query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + }.mapError { + val query = fallbackClient.query(state.snapshot.query) + + query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() }.mapSuccess { it.Media?.let { data -> State.Success(state.snapshot.query, Medium.Full(data)) @@ -45,11 +54,7 @@ class MediumStateMachine( if (cached != null) { State.Success(query, cached) } else { - if (retry <= 3) { - State.Loading(query, retry + 1) - } else { - State.Error(query) - } + State.Error(query) } } } @@ -76,8 +81,7 @@ class MediumStateMachine( sealed interface State { data class Loading( - internal val query: MediumQuery, - internal val retry: Int = 0 + internal val query: MediumQuery ) : State { constructor(id: Int) : this( MediumQuery( diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonStateMachine.kt index 0f3f1b5..fc260f5 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonStateMachine.kt @@ -6,12 +6,15 @@ import dev.datlag.aniflow.anilist.state.SeasonAction import dev.datlag.aniflow.anilist.state.SeasonState import dev.datlag.aniflow.firebase.FirebaseFactory import dev.datlag.aniflow.model.CatchResult +import dev.datlag.aniflow.model.mapError +import dev.datlag.aniflow.model.saveFirstOrNull import dev.datlag.tooling.async.suspendCatching import kotlinx.coroutines.ExperimentalCoroutinesApi @OptIn(ExperimentalCoroutinesApi::class) class PopularNextSeasonStateMachine( private val client: ApolloClient, + private val fallbackClient: ApolloClient, private val crashlytics: FirebaseFactory.Crashlytics? ) : FlowReduxStateMachine( initialState = currentState @@ -28,8 +31,14 @@ class PopularNextSeasonStateMachine( return@onEnter state.override { SeasonState.Success(query, it) } } - val response = CatchResult.result { - client.query(state.snapshot.query).execute().dataOrThrow() + val response = CatchResult.repeat(times = 2) { + val query = client.query(state.snapshot.query) + + query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + }.mapError { + val query = fallbackClient.query(state.snapshot.query) + + query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() }.mapSuccess { SeasonState.Success(state.snapshot.query, it) } @@ -38,11 +47,7 @@ class PopularNextSeasonStateMachine( response.asSuccess { crashlytics?.log(it) - if (state.snapshot.retry <= 3) { - SeasonState.Loading(state.snapshot.query, state.snapshot.retry + 1) - } else { - SeasonState.Error(query) - } + SeasonState.Error(query) } } } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonStateMachine.kt index 79535d5..3ef0e4d 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonStateMachine.kt @@ -13,6 +13,8 @@ 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.CatchResult +import dev.datlag.aniflow.model.mapError +import dev.datlag.aniflow.model.saveFirstOrNull import dev.datlag.tooling.async.suspendCatching import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.datetime.Clock @@ -23,6 +25,7 @@ import kotlinx.datetime.toLocalDateTime @OptIn(ExperimentalCoroutinesApi::class) class PopularSeasonStateMachine( private val client: ApolloClient, + private val fallbackClient: ApolloClient, private val crashlytics: FirebaseFactory.Crashlytics? ) : FlowReduxStateMachine( initialState = currentState @@ -39,8 +42,14 @@ class PopularSeasonStateMachine( return@onEnter state.override { SeasonState.Success(query, it) } } - val response = CatchResult.result { - client.query(state.snapshot.query).execute().dataOrThrow() + val response = CatchResult.repeat(times = 2) { + val query = client.query(state.snapshot.query) + + query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + }.mapError { + val query = fallbackClient.query(state.snapshot.query) + + query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() }.mapSuccess { SeasonState.Success(state.snapshot.query, it) } @@ -49,11 +58,7 @@ class PopularSeasonStateMachine( response.asSuccess { crashlytics?.log(it) - if (retry <= 3) { - SeasonState.Loading(query, retry + 1) - } else { - SeasonState.Error(query) - } + SeasonState.Error(query) } } } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt index 2139840..b60f7cc 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt @@ -8,13 +8,14 @@ import com.freeletics.flowredux.dsl.FlowReduxStateMachine 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.CatchResult +import dev.datlag.aniflow.model.* import dev.datlag.tooling.async.suspendCatching import kotlinx.coroutines.ExperimentalCoroutinesApi @OptIn(ExperimentalCoroutinesApi::class, ApolloExperimental::class) class TrendingAnimeStateMachine( private val client: ApolloClient, + private val fallbackClient: ApolloClient, private val crashlytics: FirebaseFactory.Crashlytics? ) : FlowReduxStateMachine( initialState = currentState @@ -31,8 +32,14 @@ class TrendingAnimeStateMachine( return@onEnter state.override { State.Success(query, it) } } - val response = CatchResult.result { - client.query(state.snapshot.query).execute().dataOrThrow() + val response = CatchResult.repeat(2) { + val query = client.query(state.snapshot.query) + + query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + }.mapError { + val query = fallbackClient.query(state.snapshot.query) + + query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() }.mapSuccess { State.Success(state.snapshot.query, it) } @@ -41,11 +48,7 @@ class TrendingAnimeStateMachine( response.asSuccess { crashlytics?.log(it) - if (retry <= 3) { - State.Loading(query, retry + 1) - } else { - State.Error(query) - } + State.Error(query) } } } @@ -73,8 +76,7 @@ class TrendingAnimeStateMachine( sealed interface State { data class Loading( - internal val query: TrendingQuery, - internal val retry: Int = 0 + internal val query: TrendingQuery ) : State { constructor( page: Int, diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SeasonState.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SeasonState.kt index 9fe77df..25b7a8b 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SeasonState.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SeasonState.kt @@ -13,8 +13,7 @@ import kotlinx.datetime.Instant sealed interface SeasonState { data class Loading( - internal val query: SeasonQuery, - internal val retry: Int = 0 + internal val query: SeasonQuery ) : SeasonState { constructor( page: Int, diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index eca5156..261d17b 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -1,5 +1,6 @@ - + @@ -46,6 +47,17 @@ + + + + + + + + diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt index 2dc08be..cde107f 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt @@ -6,8 +6,8 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset @@ -146,3 +146,26 @@ fun Modifier.shimmer(shape: Shape = RectangleShape): Modifier = composed { fun shimmerPainter(): BrushPainter { return BrushPainter(shimmerBrush()) } + + +@Composable +fun LazyListState.isScrollingUp(): Boolean { + var previousIndex by remember(this) { + mutableStateOf(firstVisibleItemIndex) + } + var previousScrollOffset by remember(this) { + mutableStateOf(firstVisibleItemScrollOffset) + } + return remember(this) { + derivedStateOf { + if (previousIndex != firstVisibleItemIndex) { + previousIndex > firstVisibleItemIndex + } else { + previousScrollOffset >= firstVisibleItemScrollOffset + }.also { + previousIndex = firstVisibleItemIndex + previousScrollOffset = firstVisibleItemScrollOffset + } + } + }.value +} \ 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 b734125..4e650d2 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -85,27 +85,37 @@ data object NetworkModule { }) .build() } + bindSingleton(Constants.AniList.FALLBACK_APOLLO_CLIENT) { + ApolloClient.Builder() + .dispatcher(ioDispatcher()) + .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), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), crashlytics = nullableFirebaseInstance()?.crashlytics ) } bindProvider { PopularNextSeasonStateMachine( client = instance(Constants.AniList.APOLLO_CLIENT), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), crashlytics = nullableFirebaseInstance()?.crashlytics ) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/Constants.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/Constants.kt index feb8f1b..370f748 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/Constants.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/Constants.kt @@ -5,6 +5,7 @@ data object Constants { data object AniList { const val SERVER_URL = "https://graphql.anilist.co/" const val APOLLO_CLIENT = "AniListApolloClient" + const val FALLBACK_APOLLO_CLIENT = "FallbackAniListApolloClient" data object Auth { const val BASE_URL = "https://anilist.co/api/v2/oauth/" diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt index ea873c9..a3c25fd 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt @@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CameraEnhance import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -17,7 +19,9 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.LocalPaddingValues +import dev.datlag.aniflow.common.isScrollingUp import dev.datlag.aniflow.ui.navigation.screen.initial.InitialComponent +import dev.datlag.aniflow.ui.navigation.screen.initial.model.FABConfig import dev.icerock.moko.resources.compose.stringResource @OptIn(ExperimentalFoundationApi::class, ExperimentalDecomposeApi::class, ExperimentalHazeMaterialsApi::class) @@ -51,6 +55,30 @@ fun CompactScreen(component: InitialComponent) { ) } } + }, + floatingActionButton = { + val state by FABConfig.state + + when (val current = state) { + is FABConfig.Scan -> { + ExtendedFloatingActionButton( + onClick = current.onClick, + icon = { + Icon( + imageVector = Icons.Filled.CameraEnhance, + contentDescription = null + ) + }, + text = { + Text( + text = "Scan" + ) + }, + expanded = current.listState.isScrollingUp() + ) + } + else -> { } + } } ) { CompositionLocalProvider( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt index 7c6c7a3..4960d32 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt @@ -2,6 +2,8 @@ package dev.datlag.aniflow.ui.navigation.screen.initial.component import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CameraEnhance import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -9,15 +11,42 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.extensions.compose.subscribeAsState +import dev.datlag.aniflow.common.isScrollingUp import dev.datlag.aniflow.ui.custom.ExpandedPages import dev.datlag.aniflow.ui.navigation.screen.initial.InitialComponent +import dev.datlag.aniflow.ui.navigation.screen.initial.model.FABConfig import dev.datlag.tooling.compose.EndCornerShape import dev.icerock.moko.resources.compose.stringResource @OptIn(ExperimentalDecomposeApi::class) @Composable fun ExpandedScreen(component: InitialComponent) { - Scaffold { + Scaffold( + floatingActionButton = { + val state by FABConfig.state + + when (val current = state) { + is FABConfig.Scan -> { + ExtendedFloatingActionButton( + onClick = current.onClick, + icon = { + Icon( + imageVector = Icons.Filled.CameraEnhance, + contentDescription = null + ) + }, + text = { + Text( + text = "Scan" + ) + }, + expanded = current.listState.isScrollingUp() + ) + } + else -> { } + } + } + ) { PermanentNavigationDrawer( modifier = Modifier.padding(it), drawerContent = { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt index 049cca6..e10514c 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt @@ -3,23 +3,49 @@ package dev.datlag.aniflow.ui.navigation.screen.initial.component import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding -import androidx.compose.material3.NavigationRail -import androidx.compose.material3.NavigationRailItem -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CameraEnhance +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.extensions.compose.subscribeAsState +import dev.datlag.aniflow.common.isScrollingUp import dev.datlag.aniflow.ui.custom.ExpandedPages import dev.datlag.aniflow.ui.navigation.screen.initial.InitialComponent +import dev.datlag.aniflow.ui.navigation.screen.initial.model.FABConfig import dev.icerock.moko.resources.compose.stringResource @OptIn(ExperimentalDecomposeApi::class) @Composable fun MediumScreen(component: InitialComponent) { - Scaffold { + Scaffold( + floatingActionButton = { + val state by FABConfig.state + + when (val current = state) { + is FABConfig.Scan -> { + ExtendedFloatingActionButton( + onClick = current.onClick, + icon = { + Icon( + imageVector = Icons.Filled.CameraEnhance, + contentDescription = null + ) + }, + text = { + Text( + text = "Scan" + ) + }, + expanded = current.listState.isScrollingUp() + ) + } + else -> { } + } + } + ) { Row( modifier = Modifier.padding(it) ) { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt index 421612f..9b89acd 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt @@ -6,13 +6,16 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Camera +import androidx.compose.material.icons.filled.CameraEnhance +import androidx.compose.material3.* import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -21,11 +24,13 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState import dev.chrisbanes.haze.haze import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.LocalPaddingValues +import dev.datlag.aniflow.common.isScrollingUp import dev.datlag.aniflow.common.plus import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.AiringOverview import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.PopularSeasonOverview import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.TrendingOverview +import dev.datlag.aniflow.ui.navigation.screen.initial.model.FABConfig @Composable fun HomeScreen(component: HomeComponent) { @@ -40,6 +45,13 @@ private fun MainView(component: HomeComponent, modifier: Modifier = Modifier) { initialFirstVisibleItemScrollOffset = StateSaver.List.homeOverviewOffset ) + LaunchedEffect(listState) { + FABConfig.state.value = FABConfig.Scan( + listState = listState, + onClick = { } + ) + } + LazyColumn( state = listState, modifier = modifier.haze(state = LocalHaze.current), @@ -110,6 +122,7 @@ private fun MainView(component: HomeComponent, modifier: Modifier = Modifier) { onDispose { StateSaver.List.homeOverview = listState.firstVisibleItemIndex StateSaver.List.homeOverviewOffset = listState.firstVisibleItemScrollOffset + FABConfig.state.value = null } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/model/FABConfig.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/model/FABConfig.kt new file mode 100644 index 0000000..edd35cc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/model/FABConfig.kt @@ -0,0 +1,16 @@ +package dev.datlag.aniflow.ui.navigation.screen.initial.model + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.mutableStateOf + +sealed interface FABConfig { + + data class Scan( + val listState: LazyListState, + val onClick: () -> Unit + ) : FABConfig + + companion object { + val state = mutableStateOf(null) + } +} \ No newline at end of file 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 f9eed48..875bfab 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 @@ -46,8 +46,10 @@ class MediumScreenComponent( ) : MediumComponent, 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 mediumStateMachine = MediumStateMachine( client = aniListClient, + fallbackClient = aniListFallbackClient, crashlytics = di.nullableFirebaseInstance()?.crashlytics, id = initialMedium.id ) diff --git a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/CatchResult.kt b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/CatchResult.kt index e193848..b599adf 100644 --- a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/CatchResult.kt +++ b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/CatchResult.kt @@ -1,8 +1,13 @@ package dev.datlag.aniflow.model +import dev.datlag.aniflow.model.CatchResult.Companion.result +import dev.datlag.aniflow.model.CatchResult.Success import dev.datlag.tooling.async.suspendCatching import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds sealed interface CatchResult { @@ -12,12 +17,6 @@ sealed interface CatchResult { val isError: Boolean get() = this is Error - fun onError(callback: (Throwable?) -> Unit) = apply { - if (this is Error) { - callback(this.throwable) - } - } - fun onSuccess(callback: (T & Any) -> Unit) = apply { if (this is Success) { callback(this.data) @@ -48,14 +47,6 @@ sealed interface CatchResult { } } - fun asError(onSuccess: () -> Throwable? = { null }): Throwable? { - return if (this is Error) { - this.throwable - } else { - onSuccess() - } - } - fun validate(predicate: (CatchResult) -> Boolean): CatchResult { return if (predicate(this)) { this @@ -77,19 +68,14 @@ sealed interface CatchResult { } } - suspend fun resultOnError(block: suspend CoroutineScope.() -> T): CatchResult { - return when (this) { - is Error -> result(block) - else -> this - } - } - suspend fun mapSuccess(block: suspend (T & Any) -> M?): CatchResult { return when (this) { is Success -> { block(this.data)?.let(::Success) ?: Error(null) } - else -> Error(null) + is Error -> { + Error(this.throwable) + } } } @@ -110,5 +96,59 @@ sealed interface CatchResult { } ?: Error(result.exceptionOrNull()) } } + + suspend fun repeat( + times: Int, + delayDuration: Duration = 0.seconds, + block: suspend CoroutineScope.() -> T + ): CatchResult = coroutineScope { + var result = suspendCatching(block) + var request = 1 + + while (result.isFailure && request < times) { + delay(delayDuration) + result = suspendCatching(block) + request++ + } + return@coroutineScope if (result.isFailure) { + Error(result.exceptionOrNull()) + } else { + result.getOrNull()?.let { + Success(it) + } ?: Error(result.exceptionOrNull()) + } + } + } +} + +suspend inline fun CatchResult.resultOnError(noinline block: CoroutineScope.() -> T): CatchResult { + return when (this) { + is CatchResult.Error -> result(block) + else -> this + } +} + +suspend inline fun CatchResult<*>.mapError(block: () -> M?): CatchResult { + return when (this) { + is CatchResult.Error -> { + block()?.let(::Success) ?: CatchResult.Error(null) + } + is Success -> { + (this.data as? M)?.let(::Success) ?: block()?.let(::Success) ?: CatchResult.Error(null) + } + } +} + +inline fun CatchResult.asError(onSuccess: () -> Throwable? = { null }): Throwable? { + return if (this is CatchResult.Error) { + this.throwable + } else { + onSuccess() + } +} + +inline fun CatchResult.onError(callback: (Throwable?) -> Unit) = apply { + if (this is CatchResult.Error) { + callback(this.throwable) } } \ No newline at end of file