mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 04:32:43 -05:00
add card to details screen for MCU productions with next MCU title
This commit is contained in:
10
app/src/main/java/com/owenlejeune/tvtime/api/LoadingState.kt
Normal file
10
app/src/main/java/com/owenlejeune/tvtime/api/LoadingState.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.owenlejeune.tvtime.api
|
||||
|
||||
enum class LoadingState {
|
||||
|
||||
INACTIVE,
|
||||
LOADING,
|
||||
COMPLETE,
|
||||
ERROR
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user