add card to details screen for MCU productions with next MCU title

This commit is contained in:
Owen LeJeune
2023-07-09 14:57:40 -04:00
parent 32bf5c3494
commit 080023c93e
12 changed files with 244 additions and 30 deletions

View File

@@ -0,0 +1,10 @@
package com.owenlejeune.tvtime.api
enum class LoadingState {
INACTIVE,
LOADING,
COMPLETE,
ERROR
}

View File

@@ -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<NextMCU>
}

View File

@@ -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)
}
}

View File

@@ -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<NextMCU?>(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
}
}
}

View File

@@ -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?
)

View File

@@ -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<NextMCUClient>().createNextMcuService() }
single { NextMCUService() }
single<Map<Class<*>, Any>> {
mapOf(
ListItem::class.java to ListItemDeserializer(),

View File

@@ -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")
}

View File

@@ -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(

View File

@@ -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<SpecialFeaturesViewModel>()
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,

View File

@@ -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()
}
}
}

View File

@@ -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,

View File

@@ -247,4 +247,6 @@
<string name="lists">lists</string>
<string name="no_lists_message">You don\'t have any lists yet</string>
<string name="list_count_label">%1$s (%2$d items)</string>
<string name="days_left">%1$d days</string>
<string name="next_in_the_mcu_title">Next in the MCU...</string>
</resources>