From 080023c93eaf2a68afa4eb46dfeb494ccdbf236e Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Sun, 9 Jul 2023 14:57:40 -0400 Subject: [PATCH] add card to details screen for MCU productions with next MCU title --- .../owenlejeune/tvtime/api/LoadingState.kt | 10 ++ .../tvtime/api/nextmcu/NextMCUApi.kt | 12 +++ .../tvtime/api/nextmcu/NextMCUClient.kt | 20 ++++ .../tvtime/api/nextmcu/NextMCUService.kt | 31 ++++++ .../tvtime/api/nextmcu/model/NextMCU.kt | 15 +++ .../owenlejeune/tvtime/di/modules/modules.kt | 6 ++ .../tvtime/extensions/DateExtensions.kt | 11 +++ .../owenlejeune/tvtime/ui/components/Cards.kt | 45 ++++----- .../tvtime/ui/screens/MediaDetailScreen.kt | 98 ++++++++++++++++++- .../ui/viewmodel/SpecialFeaturesViewModel.kt | 20 ++++ .../tvtime/utils/types/MediaViewType.kt | 4 +- app/src/main/res/values/strings.xml | 2 + 12 files changed, 244 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/LoadingState.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/NextMCUApi.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/NextMCUClient.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/NextMCUService.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/model/NextMCU.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/SpecialFeaturesViewModel.kt diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/LoadingState.kt b/app/src/main/java/com/owenlejeune/tvtime/api/LoadingState.kt new file mode 100644 index 0000000..d621da1 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/LoadingState.kt @@ -0,0 +1,10 @@ +package com.owenlejeune.tvtime.api + +enum class LoadingState { + + INACTIVE, + LOADING, + COMPLETE, + ERROR + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/NextMCUApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/NextMCUApi.kt new file mode 100644 index 0000000..7e1f2d7 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/NextMCUApi.kt @@ -0,0 +1,12 @@ +package com.owenlejeune.tvtime.api.nextmcu + +import com.owenlejeune.tvtime.api.nextmcu.model.NextMCU +import retrofit2.Response +import retrofit2.http.GET + +interface NextMCUApi { + + @GET("api") + suspend fun getNextMcuProject(): Response + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/NextMCUClient.kt b/app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/NextMCUClient.kt new file mode 100644 index 0000000..1ff06cf --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/NextMCUClient.kt @@ -0,0 +1,20 @@ +package com.owenlejeune.tvtime.api.nextmcu + +import com.owenlejeune.tvtime.api.Client +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.core.parameter.parametersOf + +class NextMCUClient: KoinComponent { + + companion object { + private const val BASE_URL = "https://www.whenisthenextmcufilm.com/" + } + + private val client: Client by inject { parametersOf(BASE_URL) } + + fun createNextMcuService(): NextMCUApi { + return client.create(NextMCUApi::class.java) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/NextMCUService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/NextMCUService.kt new file mode 100644 index 0000000..76f70e9 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/NextMCUService.kt @@ -0,0 +1,31 @@ +package com.owenlejeune.tvtime.api.nextmcu + +import androidx.compose.runtime.mutableStateOf +import com.owenlejeune.tvtime.api.LoadingState +import com.owenlejeune.tvtime.api.nextmcu.model.NextMCU +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class NextMCUService: KoinComponent { + + private val api: NextMCUApi by inject() + + val nextMcuProject = mutableStateOf(null) + val nextMcuLoadingStateState = mutableStateOf(LoadingState.INACTIVE) + + suspend fun getNextMcuProject() { + nextMcuLoadingStateState.value = LoadingState.LOADING + val response = api.getNextMcuProject() + if (response.isSuccessful) { + response.body()?.let { + nextMcuProject.value = it + nextMcuLoadingStateState.value = LoadingState.COMPLETE + } ?: run { + nextMcuLoadingStateState.value = LoadingState.ERROR + } + } else { + nextMcuLoadingStateState.value = LoadingState.ERROR + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/model/NextMCU.kt b/app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/model/NextMCU.kt new file mode 100644 index 0000000..0f6213a --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/nextmcu/model/NextMCU.kt @@ -0,0 +1,15 @@ +package com.owenlejeune.tvtime.api.nextmcu.model + +import com.google.gson.annotations.SerializedName +import com.owenlejeune.tvtime.utils.types.MediaViewType +import java.util.Date + +class NextMCU( + @SerializedName("days_until") val daysUntil: Int, + @SerializedName("title") val title: String, + @SerializedName("overview") val overview: String, + @SerializedName("poster_url") val posterUrl: String?, + @SerializedName("release_date") val releaseDate: Date, + @SerializedName("type") val type: MediaViewType, + @SerializedName("following_production") val followingProduction: NextMCU? +) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/di/modules/modules.kt b/app/src/main/java/com/owenlejeune/tvtime/di/modules/modules.kt index 5555041..16ee988 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/di/modules/modules.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/di/modules/modules.kt @@ -5,6 +5,8 @@ import com.google.gson.JsonDeserializer import com.google.gson.TypeAdapter import com.owenlejeune.tvtime.BuildConfig import com.owenlejeune.tvtime.api.* +import com.owenlejeune.tvtime.api.nextmcu.NextMCUClient +import com.owenlejeune.tvtime.api.nextmcu.NextMCUService import com.owenlejeune.tvtime.api.tmdb.TmdbClient import com.owenlejeune.tvtime.api.tmdb.api.v3.AccountService import com.owenlejeune.tvtime.api.tmdb.api.v3.AuthenticationService @@ -71,6 +73,10 @@ val networkModule = module { single { AuthenticationV4Service() } single { ListV4Service() } + single { NextMCUClient() } + single { get().createNextMcuService() } + single { NextMCUService() } + single, Any>> { mapOf( ListItem::class.java to ListItemDeserializer(), diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/DateExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/DateExtensions.kt index 7b6b304..cb18d35 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/extensions/DateExtensions.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/DateExtensions.kt @@ -1,10 +1,21 @@ package com.owenlejeune.tvtime.extensions +import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date +import java.util.Locale fun Date.getCalendarYear(): Int { return Calendar.getInstance().apply { time = this@getCalendarYear }.get(Calendar.YEAR) +} + +fun Date.format(format: DateFormat): String { + val formatter = SimpleDateFormat(format.format, Locale.getDefault()) + return formatter.format(this) +} + +enum class DateFormat(val format: String) { + MMMM_dd("MMMM dd") } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Cards.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Cards.kt index 4cc941a..5c1333a 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Cards.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Cards.kt @@ -1,45 +1,36 @@ package com.owenlejeune.tvtime.ui.components -import android.annotation.SuppressLint import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.* -import androidx.compose.foundation.background +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.detectHorizontalDragGestures -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.magnifier +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.FractionalThreshold -import androidx.compose.material.ThresholdConfig import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person -import androidx.compose.material.rememberSwipeableState -import androidx.compose.material.swipeable -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.owenlejeune.tvtime.R -import kotlinx.coroutines.launch -import kotlin.math.roundToInt @Composable fun ContentCard( diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt index bffd448..b99c049 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt @@ -40,13 +40,16 @@ import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.api.tmdb.api.v3.model.* +import com.owenlejeune.tvtime.extensions.DateFormat import com.owenlejeune.tvtime.extensions.WindowSizeClass +import com.owenlejeune.tvtime.extensions.format import com.owenlejeune.tvtime.extensions.getCalendarYear import com.owenlejeune.tvtime.extensions.lazyPagingItems import com.owenlejeune.tvtime.extensions.listItems import com.owenlejeune.tvtime.ui.components.* import com.owenlejeune.tvtime.ui.navigation.AppNavItem import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel +import com.owenlejeune.tvtime.ui.viewmodel.SpecialFeaturesViewModel import com.owenlejeune.tvtime.utils.SessionManager import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.utils.types.MediaViewType @@ -246,6 +249,10 @@ private fun MediaViewContent( WatchProvidersCard(itemId = itemId, type = type, mainViewModel = mainViewModel) + if (mediaItem?.productionCompanies?.firstOrNull { it.name == "Marvel Studios" } != null) { + NextMcuProjectCard(appNavController = appNavController) + } + if (windowSize != WindowSizeClass.Expanded) { ReviewsCard(itemId = itemId, type = type, mainViewModel = mainViewModel) } @@ -629,7 +636,12 @@ private fun CastCard( modifier = Modifier .padding(start = 12.dp, bottom = 12.dp) .clickable { - appNavController.navigate(AppNavItem.CaseCrewListView.withArgs(type, itemId)) + appNavController.navigate( + AppNavItem.CaseCrewListView.withArgs( + type, + itemId + ) + ) } ) } @@ -1007,6 +1019,90 @@ private fun WatchProviderContainer( } } +@Composable +private fun NextMcuProjectCard( + appNavController: NavController, + modifier: Modifier = Modifier +) { + val viewModel = viewModel() + + LaunchedEffect(Unit) { + viewModel.getNextMcuProject() + } + + val nextMcuProject = remember { viewModel.nextMcuProject } + + Card( + shape = RoundedCornerShape(10.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 10.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + modifier = modifier.then(Modifier + .fillMaxWidth() + ) + ) { + Box( + modifier = Modifier.height(250.dp) + ) { + val model = ImageRequest.Builder(LocalContext.current) + .data(nextMcuProject.value?.posterUrl) + .diskCacheKey(nextMcuProject.value?.title) + .networkCachePolicy(CachePolicy.ENABLED) + .memoryCachePolicy(CachePolicy.ENABLED) + .build() + + Column( + modifier = Modifier.padding(all = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = stringResource(R.string.next_in_the_mcu_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.W700, + modifier = Modifier.padding(start = 2.dp) + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + AsyncImage( + model = model, + modifier = Modifier + .aspectRatio(0.7f) + .clip(RoundedCornerShape(10.dp)), + contentDescription = nextMcuProject.value?.title + ) + + Column( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = nextMcuProject.value?.title ?: "", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.W700 + ) + + val releaseDate = + nextMcuProject.value?.releaseDate?.format(DateFormat.MMMM_dd) ?: "" + val daysLeft = stringResource( + id = R.string.days_left, + nextMcuProject.value?.daysUntil ?: -1 + ) + Text( + text = "$releaseDate • $daysLeft", + fontStyle = FontStyle.Italic + ) + + Text( + text = nextMcuProject.value?.overview ?: "", + overflow = TextOverflow.Ellipsis + ) + } + } + } + } + } +} + @Composable private fun ReviewsCard( itemId: Int, diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/SpecialFeaturesViewModel.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/SpecialFeaturesViewModel.kt new file mode 100644 index 0000000..5f6d462 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/SpecialFeaturesViewModel.kt @@ -0,0 +1,20 @@ +package com.owenlejeune.tvtime.ui.viewmodel + +import androidx.lifecycle.ViewModel +import com.owenlejeune.tvtime.api.nextmcu.NextMCUService +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class SpecialFeaturesViewModel: ViewModel(), KoinComponent { + + private val mcuService: NextMCUService by inject() + + val nextMcuProject = mcuService.nextMcuProject + + suspend fun getNextMcuProject() { + if (nextMcuProject.value == null) { + mcuService.getNextMcuProject() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/types/MediaViewType.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/types/MediaViewType.kt index ad3ad6b..9659c43 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/utils/types/MediaViewType.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/types/MediaViewType.kt @@ -3,9 +3,9 @@ package com.owenlejeune.tvtime.utils.types import com.google.gson.annotations.SerializedName enum class MediaViewType { - @SerializedName("movie") + @SerializedName("movie", alternate = ["Movie"]) MOVIE, - @SerializedName("tv") + @SerializedName("tv", alternate = ["TV Show"]) TV, @SerializedName("person") PERSON, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5ad8b53..e085299 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -247,4 +247,6 @@ lists You don\'t have any lists yet %1$s (%2$d items) + %1$d days + Next in the MCU... \ No newline at end of file