diff --git a/.gitignore b/.gitignore index bc0494908..c2c1bffb7 100644 --- a/.gitignore +++ b/.gitignore @@ -242,3 +242,5 @@ fabric.properties jacoco.exec .idea/ + +.kotlin/ diff --git a/android-core/src/main/kotlin/co/anitrend/core/android/compose/design/ContentWrapper.kt b/android-core/src/main/kotlin/co/anitrend/core/android/compose/design/ContentWrapper.kt index e64353baf..6e01ff3c6 100644 --- a/android-core/src/main/kotlin/co/anitrend/core/android/compose/design/ContentWrapper.kt +++ b/android-core/src/main/kotlin/co/anitrend/core/android/compose/design/ContentWrapper.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -74,25 +75,28 @@ private fun LoadingContent( config: IStateLayoutConfig, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - ContentImage(drawableResource = config.loadingDrawable) - Spacer(modifier = Modifier.height(8.dp)) - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, + Surface { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp).align(alignment = Alignment.CenterVertically), - color = MaterialTheme.colorScheme.surfaceVariant, - strokeWidth = 4.dp, - trackColor = MaterialTheme.colorScheme.secondary, - ) - config.loadingMessage?.also { - ContentText(text = stringResource(it)) + ContentImage(drawableResource = config.loadingDrawable) + Spacer(modifier = Modifier.height(8.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp) + .align(alignment = Alignment.CenterVertically), + color = MaterialTheme.colorScheme.surfaceVariant, + strokeWidth = 2.dp, + trackColor = MaterialTheme.colorScheme.secondary, + ) + config.loadingMessage?.also { + ContentText(text = stringResource(it)) + } } } } @@ -106,22 +110,24 @@ private fun ErrorContent( onClick: suspend () -> Unit, ) { val scope = rememberCoroutineScope() - Column( - modifier = modifier, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - ContentImage(drawableResource = config.errorDrawable) - Spacer(modifier = Modifier.height(16.dp)) - state.details.message?.also { - ContentText(text = it) - } - config.retryAction?.also { + Surface { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + ContentImage(drawableResource = config.errorDrawable) Spacer(modifier = Modifier.height(16.dp)) - FilledTonalButton( - onClick = { scope.launch { onClick() } }, - ) { - Text(text = stringResource(it)) + state.details.message?.also { + ContentText(text = it) + } + config.retryAction?.also { + Spacer(modifier = Modifier.height(16.dp)) + FilledTonalButton( + onClick = { scope.launch { onClick() } }, + ) { + Text(text = stringResource(it)) + } } } } @@ -174,7 +180,8 @@ fun ContentWrapper( } -@AniTrendPreview.Mobile +@AniTrendPreview.Light +@AniTrendPreview.Dark @Composable private fun ContentWrapperPreview( @PreviewParameter(provider = ContentWrapperPreviewParameter::class) loadState: LoadState @@ -187,7 +194,7 @@ private fun ContentWrapperPreview( defaultMessage = co.anitrend.core.android.R.string.app_controller_message_missing_param, retryAction = co.anitrend.core.android.R.string.action_share ) - PreviewTheme(wrapInSurface = true) { + PreviewTheme { when (loadState) { is LoadState.Error -> ErrorContent( config = config, diff --git a/app-core/src/main/kotlin/co/anitrend/core/koin/Modules.kt b/app-core/src/main/kotlin/co/anitrend/core/koin/Modules.kt index ca6e450da..093965294 100644 --- a/app-core/src/main/kotlin/co/anitrend/core/koin/Modules.kt +++ b/app-core/src/main/kotlin/co/anitrend/core/koin/Modules.kt @@ -35,7 +35,9 @@ import co.anitrend.core.coil.client.CoilRequestClient import co.anitrend.core.coil.fetch.RequestImageFetcher import co.anitrend.core.coil.mapper.RequestImageMapper import co.anitrend.data.android.koin.dataModules +import co.anitrend.data.android.network.cache.CacheHelper import co.anitrend.data.android.network.model.NetworkMessage +import co.anitrend.data.core.extensions.defaultBuilder import coil.ImageLoader import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder @@ -45,6 +47,7 @@ import coil.disk.DiskCache import coil.memory.MemoryCache import io.wax911.emojify.EmojiManager import io.wax911.emojify.serializer.kotlinx.KotlinxDeserializer +import okhttp3.CookieJar import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.koin.android.ext.koin.androidContext @@ -113,9 +116,15 @@ private val configurationModule = module { val memoryLimit = if (isLowRamDevice) 0.15 else 0.35 val storageController = get() - val client = get { - parametersOf(HttpLoggingInterceptor.Level.BASIC) - }.build() + val client = defaultBuilder() + .cookieJar(get()) + .cache( + CacheHelper.createCache( + androidContext(), + "coil-okhttp" + ) + ) + .build() val imageCache = storageController.getImageCache( context = androidContext() diff --git a/app-data-android/src/main/kotlin/co/anitrend/data/android/controller/strategy/contract/ControllerStrategy.kt b/app-data-android/src/main/kotlin/co/anitrend/data/android/controller/strategy/contract/ControllerStrategy.kt index eadb2dece..f672b2992 100644 --- a/app-data-android/src/main/kotlin/co/anitrend/data/android/controller/strategy/contract/ControllerStrategy.kt +++ b/app-data-android/src/main/kotlin/co/anitrend/data/android/controller/strategy/contract/ControllerStrategy.kt @@ -22,6 +22,7 @@ import co.anitrend.arch.request.callback.RequestCallback import co.anitrend.data.android.network.model.NetworkMessage import retrofit2.HttpException import java.net.SocketTimeoutException +import java.net.UnknownHostException /** * Contract for controller strategy @@ -49,6 +50,13 @@ abstract class ControllerStrategy { cause ) } + is UnknownHostException -> { + RequestError( + networkMessage.connectivityErrorTittle, + networkMessage.connectivityErrorMessage, + cause + ) + } else -> { if (cause == null) RequestError( diff --git a/app-data-android/src/main/kotlin/co/anitrend/data/android/controller/strategy/policy/OfflineStrategy.kt b/app-data-android/src/main/kotlin/co/anitrend/data/android/controller/strategy/policy/OfflineStrategy.kt index 53f8db24b..de433d288 100644 --- a/app-data-android/src/main/kotlin/co/anitrend/data/android/controller/strategy/policy/OfflineStrategy.kt +++ b/app-data-android/src/main/kotlin/co/anitrend/data/android/controller/strategy/policy/OfflineStrategy.kt @@ -44,7 +44,7 @@ class OfflineStrategy private constructor( ): D? { runCatching { block() - }.onSuccess {result -> + }.onSuccess { result -> callback.recordSuccess() return result }.onFailure { exception -> diff --git a/app-data-android/src/main/kotlin/co/anitrend/data/android/network/client/DeferrableNetworkClient.kt b/app-data-android/src/main/kotlin/co/anitrend/data/android/network/client/DeferrableNetworkClient.kt index 13733c3f5..8d0383057 100644 --- a/app-data-android/src/main/kotlin/co/anitrend/data/android/network/client/DeferrableNetworkClient.kt +++ b/app-data-android/src/main/kotlin/co/anitrend/data/android/network/client/DeferrableNetworkClient.kt @@ -55,8 +55,7 @@ abstract class DeferrableNetworkClient : AbstractNetworkClient exception.code() == 429 - is SocketTimeoutException, - is IOException -> true + is SocketTimeoutException -> true else -> false } diff --git a/app-data-android/src/main/kotlin/co/anitrend/data/android/network/client/OkHttpCallNetworkClient.kt b/app-data-android/src/main/kotlin/co/anitrend/data/android/network/client/OkHttpCallNetworkClient.kt index 4f34d708e..64be5d5d6 100644 --- a/app-data-android/src/main/kotlin/co/anitrend/data/android/network/client/OkHttpCallNetworkClient.kt +++ b/app-data-android/src/main/kotlin/co/anitrend/data/android/network/client/OkHttpCallNetworkClient.kt @@ -52,8 +52,7 @@ abstract class OkHttpCallNetworkClient : AbstractNetworkClient() */ override fun defaultShouldRetry(exception: Throwable) = when (exception) { is OkHttpException -> exception.code == 429 - is SocketTimeoutException, - is IOException -> true + is SocketTimeoutException -> true else -> false } @@ -118,4 +117,4 @@ abstract class OkHttpCallNetworkClient : AbstractNetworkClient() maxAttempts, shouldRetry ).bodyOrThrow() -} \ No newline at end of file +} diff --git a/app-data-android/src/main/kotlin/co/anitrend/data/android/network/client/RetrofitCallNetworkClient.kt b/app-data-android/src/main/kotlin/co/anitrend/data/android/network/client/RetrofitCallNetworkClient.kt index c9627df19..098f95bd4 100644 --- a/app-data-android/src/main/kotlin/co/anitrend/data/android/network/client/RetrofitCallNetworkClient.kt +++ b/app-data-android/src/main/kotlin/co/anitrend/data/android/network/client/RetrofitCallNetworkClient.kt @@ -46,8 +46,7 @@ internal abstract class RetrofitCallNetworkClient : AbstractNetworkClient exception.code() == 429 - is SocketTimeoutException, - is IOException -> true + is SocketTimeoutException -> true else -> false } @@ -112,4 +111,4 @@ internal abstract class RetrofitCallNetworkClient : AbstractNetworkClient() { delete from auth where user_id = :userId """) - abstract fun clearByUserId(userId: Long) + abstract suspend fun clearByUserId(userId: Long) @Query(""" select * from auth diff --git a/common-media-ui/src/main/kotlin/co/anitrend/common/media/ui/compose/item/MediaCarouselItem.kt b/common-media-ui/src/main/kotlin/co/anitrend/common/media/ui/compose/item/MediaCarouselItem.kt index f70e1c2c5..fd0cec2e4 100644 --- a/common-media-ui/src/main/kotlin/co/anitrend/common/media/ui/compose/item/MediaCarouselItem.kt +++ b/common-media-ui/src/main/kotlin/co/anitrend/common/media/ui/compose/item/MediaCarouselItem.kt @@ -112,7 +112,6 @@ fun MediaCarouselItem( items( count = carouselItems.size, key = { carouselItems[it].hashCode() }, - contentType = { carouselItems[it].carouselType }, ) { index -> val carouselItem = carouselItems[index] CarouselHeader( diff --git a/common-media-ui/src/main/kotlin/co/anitrend/common/media/ui/compose/item/MediaCompactItem.kt b/common-media-ui/src/main/kotlin/co/anitrend/common/media/ui/compose/item/MediaCompactItem.kt index ae951d31b..fad637c28 100644 --- a/common-media-ui/src/main/kotlin/co/anitrend/common/media/ui/compose/item/MediaCompactItem.kt +++ b/common-media-ui/src/main/kotlin/co/anitrend/common/media/ui/compose/item/MediaCompactItem.kt @@ -150,7 +150,7 @@ fun MediaCompactItemList( items( count = mediaItems.size, key = { mediaItems[it].hashCode() }, - contentType = { mediaItems[it].category.type } + contentType = { mediaItems[it].category } ) { index -> MediaCompactItem( media = mediaItems[index], @@ -185,7 +185,13 @@ private fun MediaCompactItemPreview() { mediaPreferenceData = MediaPreferenceData( ScoreFormat.POINT_10_DECIMAL, ), - mediaItemClick = {} + mediaItemClick = {}, + modifier = Modifier + .padding(8.dp) + .size( + height = 265.dp, + width = 150.dp, + ), ) } } diff --git a/feature-media-carousel/src/main/kotlin/co/anitrend/media/carousel/component/content/CarouselContent.kt b/feature-media-carousel/src/main/kotlin/co/anitrend/media/carousel/component/content/CarouselContent.kt index 12b4e5147..48abda06b 100644 --- a/feature-media-carousel/src/main/kotlin/co/anitrend/media/carousel/component/content/CarouselContent.kt +++ b/feature-media-carousel/src/main/kotlin/co/anitrend/media/carousel/component/content/CarouselContent.kt @@ -20,6 +20,8 @@ package co.anitrend.media.carousel.component.content import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup +import androidx.lifecycle.liveData +import co.anitrend.arch.domain.entities.LoadState import co.anitrend.arch.extension.ext.argument import co.anitrend.common.media.ui.controller.extensions.openMediaListSheetFor import co.anitrend.core.android.compose.design.ContentWrapper @@ -36,6 +38,7 @@ import co.anitrend.navigation.MediaDiscoverRouter import co.anitrend.navigation.MediaListEditorRouter import co.anitrend.navigation.MediaRouter import co.anitrend.navigation.extensions.asNavPayload +import co.anitrend.navigation.extensions.nameOf import co.anitrend.navigation.extensions.startActivity import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber @@ -46,7 +49,9 @@ class CarouselContent( ) : AniTrendComposition() { private val viewModel by viewModel() - private val param by argument() + private val param by argument( + key = nameOf() + ) private fun paramOrDefault(): MediaCarouselRouter.MediaCarouselRouterParam { return param ?: MediaCarouselRouter.MediaCarouselRouterParam( @@ -89,9 +94,9 @@ class CarouselContent( ) = composable(context = inflater.context) { AniTrendTheme3 { ContentWrapper( - stateFlow = viewModelState().combinedLoadState, + stateFlow = liveData { emit(LoadState.Idle()) }, param = paramOrDefault(), - onLoad = viewModelState()::invoke, + onLoad = viewModel::invoke, onClick = viewModelState()::retry, ) { CarouselScreen( diff --git a/feature-media-carousel/src/main/kotlin/co/anitrend/media/carousel/component/viewmodel/CarouselViewModel.kt b/feature-media-carousel/src/main/kotlin/co/anitrend/media/carousel/component/viewmodel/CarouselViewModel.kt index bbbbf441f..a8e265301 100644 --- a/feature-media-carousel/src/main/kotlin/co/anitrend/media/carousel/component/viewmodel/CarouselViewModel.kt +++ b/feature-media-carousel/src/main/kotlin/co/anitrend/media/carousel/component/viewmodel/CarouselViewModel.kt @@ -21,12 +21,13 @@ import androidx.lifecycle.viewModelScope import co.anitrend.core.component.viewmodel.AniTrendViewModel import co.anitrend.domain.carousel.model.CarouselParam import co.anitrend.media.carousel.component.viewmodel.state.CarouselState +import co.anitrend.navigation.MediaCarouselRouter import kotlinx.coroutines.launch class CarouselViewModel( override val state: CarouselState ) : AniTrendViewModel() { - operator fun invoke(param: CarouselParam.Find) { + operator fun invoke(param: MediaCarouselRouter.MediaCarouselRouterParam) { viewModelScope.launch { state(param) } diff --git a/feature-media-carousel/src/main/kotlin/co/anitrend/media/carousel/component/viewmodel/state/CarouselState.kt b/feature-media-carousel/src/main/kotlin/co/anitrend/media/carousel/component/viewmodel/state/CarouselState.kt index f9fa1ad78..816313bfc 100644 --- a/feature-media-carousel/src/main/kotlin/co/anitrend/media/carousel/component/viewmodel/state/CarouselState.kt +++ b/feature-media-carousel/src/main/kotlin/co/anitrend/media/carousel/component/viewmodel/state/CarouselState.kt @@ -27,7 +27,7 @@ data class CarouselState( private val interactor: GetCarouselInteractor ) : AniTrendViewModelState>() { - operator fun invoke(param: MediaCarouselRouter.MediaCarouselRouterParam) { + suspend operator fun invoke(param: MediaCarouselRouter.MediaCarouselRouterParam) { val input = CarouselParam.Find( season = param.season, seasonYear = param.seasonYear, diff --git a/feature-search/src/main/kotlin/co/anitrend/search/component/screen/SearchScreen.kt b/feature-search/src/main/kotlin/co/anitrend/search/component/screen/SearchScreen.kt index db7625e9c..f8d69696d 100644 --- a/feature-search/src/main/kotlin/co/anitrend/search/component/screen/SearchScreen.kt +++ b/feature-search/src/main/kotlin/co/anitrend/search/component/screen/SearchScreen.kt @@ -36,8 +36,9 @@ import co.anitrend.search.component.presenter.SearchPresenter class SearchScreen : AniTrendScreen() { private val presenter by inject() - private val param by extra( - key = nameOf() + private val param by extra( + key = nameOf(), + default = SearchRouter::SearchParam , ) override fun onCreate(savedInstanceState: Bundle?) {