mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-12 14:52:44 -05:00
small fixes + new action buttons
This commit is contained in:
@@ -14,7 +14,7 @@ import kotlin.reflect.KClass
|
||||
val networkModule = module {
|
||||
single { if (BuildConfig.DEBUG) DebugHttpClient() else ProdHttpClient() }
|
||||
single<Converter> { GsonConverter() }
|
||||
single { (baseUrl: String) -> Client(baseUrl) }
|
||||
factory { (baseUrl: String) -> Client(baseUrl) }
|
||||
|
||||
single<Map<Class<*>, JsonDeserializer<*>>> { mapOf(ListItem::class.java to ListItemDeserializer()) }
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -23,6 +24,7 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
@@ -150,7 +152,6 @@ fun PosterItem(
|
||||
elevation: Dp = 8.dp,
|
||||
contentDescription: String?
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Card(
|
||||
elevation = elevation,
|
||||
modifier = modifier
|
||||
@@ -161,24 +162,26 @@ fun PosterItem(
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.size(width = width, height = height)
|
||||
.clip(RoundedCornerShape(5f.dpToPx(context)))
|
||||
.clip(RoundedCornerShape(5.dp))
|
||||
.clickable(
|
||||
onClick = onClick
|
||||
),
|
||||
model = url,
|
||||
placeholder = painterResource(id = placeholder),
|
||||
contentDescription = contentDescription
|
||||
placeholder = rememberAsyncImagePainter(model = placeholder),
|
||||
contentDescription = contentDescription,
|
||||
contentScale = ContentScale.FillBounds
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(width = width, height = height)
|
||||
.clip(RoundedCornerShape(5f.dpToPx(context)))
|
||||
.clip(RoundedCornerShape(5.dp))
|
||||
.clickable(
|
||||
onClick = onClick
|
||||
),
|
||||
painter = painterResource(id = noDataImage),
|
||||
contentDescription = contentDescription
|
||||
painter = rememberAsyncImagePainter(model = noDataImage),
|
||||
contentDescription = contentDescription,
|
||||
contentScale = ContentScale.FillBounds
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -212,7 +215,7 @@ fun BackdropImage(
|
||||
val backdrop = collection.backdrops[page]
|
||||
AsyncImage(
|
||||
model = TmdbUtils.getFullBackdropPath(backdrop),
|
||||
placeholder = painterResource(id = R.drawable.placeholder),
|
||||
placeholder = rememberAsyncImagePainter(model = R.drawable.placeholder),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.onGloballyPositioned { sizeImage = it.size }
|
||||
)
|
||||
@@ -221,15 +224,17 @@ fun BackdropImage(
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
placeholder = painterResource(id = R.drawable.placeholder),
|
||||
placeholder = rememberAsyncImagePainter(model = R.drawable.placeholder),
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.onGloballyPositioned { sizeImage = it.size }
|
||||
modifier = Modifier.onGloballyPositioned { sizeImage = it.size },
|
||||
contentScale = ContentScale.FillWidth
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.placeholder),
|
||||
painter = rememberAsyncImagePainter(model = R.drawable.placeholder),
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.onGloballyPositioned { sizeImage = it.size }
|
||||
modifier = Modifier.onGloballyPositioned { sizeImage = it.size },
|
||||
contentScale = ContentScale.FillWidth
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.text.HtmlCompat
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AuthorDetails
|
||||
@@ -551,7 +552,7 @@ fun FullScreenThumbnailVideoPlayer(
|
||||
),
|
||||
model = "https://img.youtube.com/vi/${key}/hqdefault.jpg",
|
||||
contentDescription = "",
|
||||
placeholder = painterResource(id = R.drawable.placeholder)
|
||||
placeholder = rememberAsyncImagePainter(model = R.drawable.placeholder)
|
||||
)
|
||||
|
||||
if (showFullscreenView.value) {
|
||||
@@ -899,4 +900,28 @@ fun LinkableText(
|
||||
onTextLayout = onTextLayout,
|
||||
style = style
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TimeoutSnackbar(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
timeoutMillis: Long = 400,
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
var snackbarVisible by remember { mutableStateOf(true) }
|
||||
|
||||
if (snackbarVisible) {
|
||||
Snackbar(
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(text = text)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(timeoutMillis)
|
||||
snackbarVisible = false
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ sealed class AccountTabNavItem(stringRes: Int, route: String, val mediaType: Med
|
||||
object RatedTvEpisodes: AccountTabNavItem(R.string.nav_rated_episodes_title, "rated_episodes_route", MediaViewType.EPISODE, screenContent, { SessionManager.currentSession?.ratedTvEpisodes ?: emptyList() }, RatedEpisode::class)
|
||||
// object Lists
|
||||
object FavoriteMovies: AccountTabNavItem(R.string.nav_favorite_movies_title, "favorite_movies_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession?.favoriteMovies ?: emptyList() }, FavoriteMovie::class)
|
||||
object FavoriteTvShows: AccountTabNavItem(R.string.nav_favorite_tv_show_title, "favorite_shows_route", MediaViewType.TV, screenContent, { SessionManager.currentSession?.favoriteMovies ?: emptyList() }, FavoriteTvSeries::class)
|
||||
object FavoriteTvShows: AccountTabNavItem(R.string.nav_favorite_tv_show_title, "favorite_shows_route", MediaViewType.TV, screenContent, { SessionManager.currentSession?.favoriteTvShows ?: emptyList() }, FavoriteTvSeries::class)
|
||||
object MovieWatchlist: AccountTabNavItem(R.string.nav_movie_watchlist_title, "movie_watchlist_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession?.movieWatchlist ?: emptyList() }, WatchlistMovie::class)
|
||||
object TvWatchlist: AccountTabNavItem(R.string.nav_tv_watchlist_title, "tv_watchlist_route", MediaViewType.TV, screenContent, { SessionManager.currentSession?.tvWatchlist ?: emptyList() }, WatchlistTvSeries::class)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ private fun Backdrop(modifier: Modifier, imageUrl: String?, contentDescription:
|
||||
BackdropImage(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(280.dp),
|
||||
.height(230.dp),
|
||||
imageUrl = TmdbUtils.getFullBackdropPath(imageUrl),
|
||||
contentDescription = contentDescription
|
||||
// collection = images.value
|
||||
|
||||
@@ -2,9 +2,15 @@ package com.owenlejeune.tvtime.ui.screens
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.keyframes
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
@@ -13,6 +19,7 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -22,10 +29,12 @@ import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.AccountService
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.DetailService
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService
|
||||
@@ -33,14 +42,13 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
|
||||
import com.owenlejeune.tvtime.extensions.listItems
|
||||
import com.owenlejeune.tvtime.ui.components.*
|
||||
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
|
||||
import com.owenlejeune.tvtime.ui.theme.FavoriteSelected
|
||||
import com.owenlejeune.tvtime.ui.theme.RatingSelected
|
||||
import com.owenlejeune.tvtime.ui.theme.WatchlistSelected
|
||||
import com.owenlejeune.tvtime.ui.theme.actionButtonColor
|
||||
import com.owenlejeune.tvtime.utils.SessionManager
|
||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.*
|
||||
import org.json.JSONObject
|
||||
import java.text.DecimalFormat
|
||||
|
||||
@@ -82,7 +90,8 @@ fun MediaDetailView(
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
// horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (type == MediaViewType.MOVIE) {
|
||||
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
|
||||
@@ -90,6 +99,8 @@ fun MediaDetailView(
|
||||
MiscTvDetails(mediaItem = mediaItem, service as TvService)
|
||||
}
|
||||
|
||||
ActionsView(itemId = itemId, type = type, service = service)
|
||||
|
||||
OverviewCard(itemId = itemId, mediaItem = mediaItem, service = service)
|
||||
|
||||
CastCard(itemId = itemId, service = service, appNavController = appNavController)
|
||||
@@ -98,8 +109,6 @@ fun MediaDetailView(
|
||||
|
||||
VideosCard(itemId = itemId, service = service)
|
||||
|
||||
ActionsView(itemId = itemId, type = type, service = service)
|
||||
|
||||
ReviewsCard(itemId = itemId, service = service)
|
||||
}
|
||||
|
||||
@@ -162,7 +171,6 @@ private fun MiscDetails(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Text(text = year, color = MaterialTheme.colorScheme.onBackground)
|
||||
Text(
|
||||
@@ -197,26 +205,29 @@ private fun ActionsView(
|
||||
val session = SessionManager.currentSession
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
.wrapContentSize(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
RateButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
itemId = itemId,
|
||||
type = type,
|
||||
service = service
|
||||
)
|
||||
|
||||
if (session?.isAuthorized == true) {
|
||||
ActionButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.add_to_list_action_label),
|
||||
onClick = { /*TODO*/ }
|
||||
val accountService = AccountService()
|
||||
WatchlistButton(
|
||||
itemId = itemId,
|
||||
type = type
|
||||
)
|
||||
ActionButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.favourite_label),
|
||||
onClick = { /*TODO*/ }
|
||||
ListButton(
|
||||
itemId = itemId,
|
||||
type = type,
|
||||
service = accountService
|
||||
)
|
||||
FavoriteButton(
|
||||
itemId = itemId,
|
||||
type = type
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -224,15 +235,17 @@ private fun ActionsView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionButton(modifier: Modifier, text: String, onClick: () -> Unit) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.tertiary),
|
||||
onClick = onClick
|
||||
) {
|
||||
Text(text = text)
|
||||
}
|
||||
private fun ActionButton(
|
||||
iconRes: Int,
|
||||
contentDescription: String,
|
||||
isSelected: Boolean,
|
||||
filledIconColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
unfilledIconColor: Color = MaterialTheme.colorScheme.background,
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.actionButtonColor,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
// TODO - refactor buttons here
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -255,70 +268,217 @@ private fun RateButton(
|
||||
)
|
||||
}
|
||||
|
||||
val showSessionDialog = remember { mutableStateOf(false) }
|
||||
val showRatingDialog = remember { mutableStateOf(false) }
|
||||
|
||||
val bgColor = MaterialTheme.colorScheme.background
|
||||
val filledColor = RatingSelected
|
||||
val tintColor = remember { Animatable(if (itemIsRated.value) filledColor else bgColor) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.animateContentSize(tween(durationMillis = 300))
|
||||
.clip(CircleShape)
|
||||
.height(40.dp)
|
||||
.requiredWidthIn(min = 40.dp)
|
||||
.background(color = MaterialTheme.colorScheme.actionButtonColor)
|
||||
.clickable(
|
||||
onClick = {
|
||||
if (session == null) {
|
||||
showSessionDialog.value = true
|
||||
} else {
|
||||
showRatingDialog.value = true
|
||||
}
|
||||
}
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.align(Alignment.Center),
|
||||
painter = painterResource(id = R.drawable.ic_rating_star),
|
||||
contentDescription = "",
|
||||
tint = tintColor.value
|
||||
)
|
||||
}
|
||||
|
||||
CreateSessionDialog(showDialog = showSessionDialog, onSessionReturned = {})
|
||||
|
||||
val userRating = session?.getRatingForId(itemId) ?: 0f
|
||||
RatingDialog(showDialog = showRatingDialog, rating = userRating, onValueConfirmed = { rating ->
|
||||
if (rating > 0f) {
|
||||
postRating(context, rating, itemId, service, itemIsRated)
|
||||
coroutineScope.launch {
|
||||
tintColor.animateTo(targetValue = filledColor, animationSpec = tween(300))
|
||||
}
|
||||
} else {
|
||||
deleteRating(context, itemId, service, itemIsRated)
|
||||
coroutineScope.launch {
|
||||
tintColor.animateTo(targetValue = bgColor, animationSpec = tween(300))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchlistButton(
|
||||
itemId: Int,
|
||||
type: MediaViewType,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val session = SessionManager.currentSession
|
||||
|
||||
val hasWatchlistedItem = remember {
|
||||
mutableStateOf(
|
||||
if (type == MediaViewType.MOVIE) {
|
||||
session?.hasWatchlistedMovie(itemId) == true
|
||||
} else {
|
||||
session?.hasWatchlistedTvShow(itemId) == true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val showSessionDialog = remember { mutableStateOf(false) }
|
||||
|
||||
val bgColor = MaterialTheme.colorScheme.background
|
||||
val filledColor = WatchlistSelected
|
||||
val tintColor = remember { Animatable(if (hasWatchlistedItem.value) filledColor else bgColor) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.animateContentSize(tween(durationMillis = 300))
|
||||
.clip(CircleShape)
|
||||
.height(40.dp)
|
||||
.requiredWidthIn(min = 40.dp)
|
||||
.background(color = MaterialTheme.colorScheme.actionButtonColor)
|
||||
.clickable(
|
||||
onClick = {
|
||||
if (session == null) {
|
||||
showSessionDialog.value = true
|
||||
} else {
|
||||
addToWatchlist(context, itemId, type, hasWatchlistedItem) { added ->
|
||||
coroutineScope.launch {
|
||||
tintColor.animateTo(
|
||||
targetValue = if (added) filledColor else bgColor,
|
||||
animationSpec = tween(300)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.align(Alignment.Center),
|
||||
painter = painterResource(id = R.drawable.ic_watchlist),
|
||||
contentDescription = "",
|
||||
tint = tintColor.value
|
||||
)
|
||||
}
|
||||
|
||||
CreateSessionDialog(showDialog = showSessionDialog, onSessionReturned = {})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListButton(
|
||||
itemId: Int,
|
||||
type: MediaViewType,
|
||||
modifier: Modifier = Modifier,
|
||||
service: AccountService
|
||||
) {
|
||||
val session = SessionManager.currentSession
|
||||
|
||||
// val hasListedItem
|
||||
|
||||
val showSessionDialog = remember { mutableStateOf(false) }
|
||||
|
||||
CircleBackgroundColorImage(
|
||||
modifier = modifier.clickable(
|
||||
onClick = {
|
||||
if (SessionManager.currentSession != null) {
|
||||
showRatingDialog.value = true
|
||||
} else {
|
||||
if (session == null) {
|
||||
showSessionDialog.value = true
|
||||
} else {
|
||||
// add to watchlsit
|
||||
}
|
||||
}
|
||||
),
|
||||
size = 40.dp,
|
||||
backgroundColor = MaterialTheme.colorScheme.actionButtonColor,
|
||||
painter = painterResource(id = R.drawable.ic_rating_star),
|
||||
colorFilter = ColorFilter.tint(color = if (itemIsRated.value) RatingSelected else MaterialTheme.colorScheme.background),
|
||||
painter = painterResource(id = R.drawable.ic_add_to_list),
|
||||
colorFilter = ColorFilter.tint(color = /*if (hasWatchlistedItem.value) WatchlistSelected else*/ MaterialTheme.colorScheme.background),
|
||||
contentDescription = ""
|
||||
)
|
||||
|
||||
RatingDialog(showDialog = showRatingDialog, onValueConfirmed = { rating ->
|
||||
if (rating > 0f) {
|
||||
postRating(context, rating, itemId, service, itemIsRated)
|
||||
} else {
|
||||
deleteRating(context, itemId, service, itemIsRated)
|
||||
}
|
||||
})
|
||||
|
||||
CreateSessionDialog(showDialog = showSessionDialog, onSessionReturned = {
|
||||
showRatingDialog.value = it
|
||||
})
|
||||
CreateSessionDialog(showDialog = showSessionDialog, onSessionReturned = {})
|
||||
}
|
||||
|
||||
fun postRating(context: Context, rating: Float, itemId: Int, service: DetailService, itemIsRated: MutableState<Boolean>) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = service.postRating(itemId, RatingBody(rating = rating))
|
||||
if (response.isSuccessful) {
|
||||
SessionManager.currentSession?.refresh(changed = arrayOf(SessionManager.Session.Changed.RatedMovies, SessionManager.Session.Changed.RatedTv))
|
||||
withContext(Dispatchers.Main) {
|
||||
itemIsRated.value = true
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun FavoriteButton(
|
||||
itemId: Int,
|
||||
type: MediaViewType,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val session = SessionManager.currentSession
|
||||
|
||||
val hasFavorited = remember {
|
||||
mutableStateOf(
|
||||
if (type == MediaViewType.MOVIE) {
|
||||
session?.hasFavoritedMovie(itemId) == true
|
||||
} else {
|
||||
session?.hasFavoritedTvShow(itemId) == true
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
val errorObj = JSONObject(response.errorBody().toString())
|
||||
Toast.makeText(context, "Error: ${errorObj.getString("status_message")}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteRating(context: Context, itemId: Int, service: DetailService, itemIsRated: MutableState<Boolean>) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = service.deleteRating(itemId)
|
||||
if (response.isSuccessful) {
|
||||
SessionManager.currentSession?.refresh(changed = arrayOf(SessionManager.Session.Changed.RatedMovies, SessionManager.Session.Changed.RatedTv))
|
||||
withContext(Dispatchers.Main) {
|
||||
itemIsRated.value = false
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
val errorObj = JSONObject(response.errorBody().toString())
|
||||
Toast.makeText(context, "Error: ${errorObj.getString("status_message")}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
val showSessionDialog = remember { mutableStateOf(false) }
|
||||
|
||||
val bgColor = MaterialTheme.colorScheme.background
|
||||
val filledColor = FavoriteSelected
|
||||
val tintColor = remember { Animatable(if (hasFavorited.value) filledColor else bgColor) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.animateContentSize(tween(durationMillis = 300))
|
||||
.clip(CircleShape)
|
||||
.height(40.dp)
|
||||
.requiredWidthIn(min = 40.dp)
|
||||
.background(color = MaterialTheme.colorScheme.actionButtonColor)
|
||||
.clickable(
|
||||
onClick = {
|
||||
if (session == null) {
|
||||
showSessionDialog.value = true
|
||||
} else {
|
||||
addToFavorite(context, itemId, type, hasFavorited) { added ->
|
||||
coroutineScope.launch {
|
||||
tintColor.animateTo(
|
||||
targetValue = if (added) filledColor else bgColor,
|
||||
animationSpec = tween(300)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.align(Alignment.Center),
|
||||
painter = painterResource(id = R.drawable.ic_favorite),
|
||||
contentDescription = "",
|
||||
tint = tintColor.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,14 +533,14 @@ private fun CreateSessionDialog(showDialog: MutableState<Boolean>, onSessionRetu
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RatingDialog(showDialog: MutableState<Boolean>, onValueConfirmed: (Float) -> Unit) {
|
||||
private fun RatingDialog(showDialog: MutableState<Boolean>, rating: Float, onValueConfirmed: (Float) -> Unit) {
|
||||
|
||||
fun formatPosition(position: Float): String {
|
||||
return DecimalFormat("#.#").format(position.toInt()*5/10f)
|
||||
}
|
||||
|
||||
if (showDialog.value) {
|
||||
var sliderPosition by remember { mutableStateOf(0f) }
|
||||
var sliderPosition by remember { mutableStateOf(rating) }
|
||||
val formatted = formatPosition(sliderPosition).toFloat()
|
||||
AlertDialog(
|
||||
modifier = Modifier.wrapContentHeight(),
|
||||
@@ -884,4 +1044,98 @@ private fun fetchKeywords(id: Int, service: DetailService, keywordsResponse: Mut
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun postRating(context: Context, rating: Float, itemId: Int, service: DetailService, itemIsRated: MutableState<Boolean>) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = service.postRating(itemId, RatingBody(rating = rating))
|
||||
if (response.isSuccessful) {
|
||||
SessionManager.currentSession?.refresh(changed = arrayOf(SessionManager.Session.Changed.RatedMovies, SessionManager.Session.Changed.RatedTv))
|
||||
withContext(Dispatchers.Main) {
|
||||
itemIsRated.value = true
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
val errorObj = JSONObject(response.errorBody().toString())
|
||||
Toast.makeText(context, "Error: ${errorObj.getString("status_message")}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteRating(context: Context, itemId: Int, service: DetailService, itemIsRated: MutableState<Boolean>) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = service.deleteRating(itemId)
|
||||
if (response.isSuccessful) {
|
||||
SessionManager.currentSession?.refresh(changed = SessionManager.Session.Changed.Rated)
|
||||
withContext(Dispatchers.Main) {
|
||||
itemIsRated.value = false
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
val errorObj = JSONObject(response.errorBody().toString())
|
||||
Toast.makeText(context, "Error: ${errorObj.getString("status_message")}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToWatchlist(
|
||||
context: Context,
|
||||
itemId: Int,
|
||||
type: MediaViewType,
|
||||
itemIsWatchlisted: MutableState<Boolean>,
|
||||
onWatchlistChanged: (Boolean) -> Unit
|
||||
) {
|
||||
val accountId = SessionManager.currentSession!!.accountDetails!!.id
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = AccountService().addToWatchlist(accountId, WatchlistBody(type, itemId, !itemIsWatchlisted.value))
|
||||
if (response.isSuccessful) {
|
||||
SessionManager.currentSession?.refresh(changed = SessionManager.Session.Changed.Watchlist)
|
||||
withContext(Dispatchers.Main) {
|
||||
itemIsWatchlisted.value = !itemIsWatchlisted.value
|
||||
onWatchlistChanged(itemIsWatchlisted.value)
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "An error occurred", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToFavorite(
|
||||
context: Context,
|
||||
itemId: Int,
|
||||
type: MediaViewType,
|
||||
itemIsFavorited: MutableState<Boolean>,
|
||||
onFavoriteChanged: (Boolean) -> Unit
|
||||
) {
|
||||
val accountId = SessionManager.currentSession!!.accountDetails!!.id
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = AccountService().markAsFavorite(accountId, MarkAsFavoriteBody(type, itemId, !itemIsFavorited.value))
|
||||
if (response.isSuccessful) {
|
||||
SessionManager.currentSession?.refresh(changed = SessionManager.Session.Changed.Favorites)
|
||||
withContext(Dispatchers.Main) {
|
||||
itemIsFavorited.value = !itemIsFavorited.value
|
||||
onFavoriteChanged(itemIsFavorited.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionSnackBar(
|
||||
message: String
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,8 @@ private val LightColorPalette = lightColorScheme(
|
||||
fun TVTimeTheme(
|
||||
isDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||
isDynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit) {
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
val colorScheme = when {
|
||||
dynamicColor && isDarkTheme -> {
|
||||
|
||||
@@ -140,6 +140,13 @@ object SessionManager: KoinComponent {
|
||||
return ratedTvEpisodes.map { it.id }.contains(id)
|
||||
}
|
||||
|
||||
fun getRatingForId(id: Int): Float {
|
||||
return ratedMovies.firstOrNull { it.id == id }?.rating
|
||||
?: ratedTvShows.firstOrNull { it.id == id }?.rating
|
||||
?: ratedTvEpisodes.firstOrNull { it.id == id }?.rating
|
||||
?: 0f
|
||||
}
|
||||
|
||||
fun hasFavoritedMovie(id: Int): Boolean {
|
||||
return favoriteMovies.map { it.id }.contains(id)
|
||||
}
|
||||
@@ -173,6 +180,9 @@ object SessionManager: KoinComponent {
|
||||
|
||||
companion object {
|
||||
val All get() = values()
|
||||
val Rated get() = arrayOf(RatedMovies, RatedTv, RatedEpisodes)
|
||||
val Favorites get() = arrayOf(FavoriteMovies, FavoriteTv)
|
||||
val Watchlist get() = arrayOf(WatchlistMovies, WatchlistTv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="20dp" />
|
||||
<solid android:color="@android:color/darker_gray" />
|
||||
</shape>
|
||||
Reference in New Issue
Block a user