mirror of
https://github.com/owenlejeune/TVTime.git
synced 2026-01-06 14:51:18 -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.google.gson.TypeAdapter
|
||||||
import com.owenlejeune.tvtime.BuildConfig
|
import com.owenlejeune.tvtime.BuildConfig
|
||||||
import com.owenlejeune.tvtime.api.*
|
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.TmdbClient
|
||||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.AccountService
|
import com.owenlejeune.tvtime.api.tmdb.api.v3.AccountService
|
||||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.AuthenticationService
|
import com.owenlejeune.tvtime.api.tmdb.api.v3.AuthenticationService
|
||||||
@@ -71,6 +73,10 @@ val networkModule = module {
|
|||||||
single { AuthenticationV4Service() }
|
single { AuthenticationV4Service() }
|
||||||
single { ListV4Service() }
|
single { ListV4Service() }
|
||||||
|
|
||||||
|
single { NextMCUClient() }
|
||||||
|
single { get<NextMCUClient>().createNextMcuService() }
|
||||||
|
single { NextMCUService() }
|
||||||
|
|
||||||
single<Map<Class<*>, Any>> {
|
single<Map<Class<*>, Any>> {
|
||||||
mapOf(
|
mapOf(
|
||||||
ListItem::class.java to ListItemDeserializer(),
|
ListItem::class.java to ListItemDeserializer(),
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
package com.owenlejeune.tvtime.extensions
|
package com.owenlejeune.tvtime.extensions
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
fun Date.getCalendarYear(): Int {
|
fun Date.getCalendarYear(): Int {
|
||||||
return Calendar.getInstance().apply {
|
return Calendar.getInstance().apply {
|
||||||
time = this@getCalendarYear
|
time = this@getCalendarYear
|
||||||
}.get(Calendar.YEAR)
|
}.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
|
package com.owenlejeune.tvtime.ui.components
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.magnifier
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Person
|
import androidx.compose.material.icons.filled.Person
|
||||||
import androidx.compose.material.rememberSwipeableState
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material.swipeable
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.*
|
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.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
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.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.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.owenlejeune.tvtime.R
|
import com.owenlejeune.tvtime.R
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ContentCard(
|
fun ContentCard(
|
||||||
|
|||||||
@@ -40,13 +40,16 @@ import com.google.accompanist.pager.rememberPagerState
|
|||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import com.owenlejeune.tvtime.R
|
import com.owenlejeune.tvtime.R
|
||||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
|
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.WindowSizeClass
|
||||||
|
import com.owenlejeune.tvtime.extensions.format
|
||||||
import com.owenlejeune.tvtime.extensions.getCalendarYear
|
import com.owenlejeune.tvtime.extensions.getCalendarYear
|
||||||
import com.owenlejeune.tvtime.extensions.lazyPagingItems
|
import com.owenlejeune.tvtime.extensions.lazyPagingItems
|
||||||
import com.owenlejeune.tvtime.extensions.listItems
|
import com.owenlejeune.tvtime.extensions.listItems
|
||||||
import com.owenlejeune.tvtime.ui.components.*
|
import com.owenlejeune.tvtime.ui.components.*
|
||||||
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
|
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
|
||||||
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
|
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.SessionManager
|
||||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||||
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||||
@@ -246,6 +249,10 @@ private fun MediaViewContent(
|
|||||||
|
|
||||||
WatchProvidersCard(itemId = itemId, type = type, mainViewModel = mainViewModel)
|
WatchProvidersCard(itemId = itemId, type = type, mainViewModel = mainViewModel)
|
||||||
|
|
||||||
|
if (mediaItem?.productionCompanies?.firstOrNull { it.name == "Marvel Studios" } != null) {
|
||||||
|
NextMcuProjectCard(appNavController = appNavController)
|
||||||
|
}
|
||||||
|
|
||||||
if (windowSize != WindowSizeClass.Expanded) {
|
if (windowSize != WindowSizeClass.Expanded) {
|
||||||
ReviewsCard(itemId = itemId, type = type, mainViewModel = mainViewModel)
|
ReviewsCard(itemId = itemId, type = type, mainViewModel = mainViewModel)
|
||||||
}
|
}
|
||||||
@@ -629,7 +636,12 @@ private fun CastCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 12.dp, bottom = 12.dp)
|
.padding(start = 12.dp, bottom = 12.dp)
|
||||||
.clickable {
|
.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
|
@Composable
|
||||||
private fun ReviewsCard(
|
private fun ReviewsCard(
|
||||||
itemId: Int,
|
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
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
enum class MediaViewType {
|
enum class MediaViewType {
|
||||||
@SerializedName("movie")
|
@SerializedName("movie", alternate = ["Movie"])
|
||||||
MOVIE,
|
MOVIE,
|
||||||
@SerializedName("tv")
|
@SerializedName("tv", alternate = ["TV Show"])
|
||||||
TV,
|
TV,
|
||||||
@SerializedName("person")
|
@SerializedName("person")
|
||||||
PERSON,
|
PERSON,
|
||||||
|
|||||||
@@ -247,4 +247,6 @@
|
|||||||
<string name="lists">lists</string>
|
<string name="lists">lists</string>
|
||||||
<string name="no_lists_message">You don\'t have any lists yet</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="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>
|
</resources>
|
||||||
Reference in New Issue
Block a user