small fixes + new action buttons

This commit is contained in:
Owen LeJeune
2022-03-18 12:24:39 -04:00
parent 1f8eeb6f2b
commit 8708ffcd80
9 changed files with 386 additions and 92 deletions

View File

@@ -14,7 +14,7 @@ import kotlin.reflect.KClass
val networkModule = module { val networkModule = module {
single { if (BuildConfig.DEBUG) DebugHttpClient() else ProdHttpClient() } single { if (BuildConfig.DEBUG) DebugHttpClient() else ProdHttpClient() }
single<Converter> { GsonConverter() } single<Converter> { GsonConverter() }
single { (baseUrl: String) -> Client(baseUrl) } factory { (baseUrl: String) -> Client(baseUrl) }
single<Map<Class<*>, JsonDeserializer<*>>> { mapOf(ListItem::class.java to ListItemDeserializer()) } single<Map<Class<*>, JsonDeserializer<*>>> { mapOf(ListItem::class.java to ListItemDeserializer()) }
} }

View File

@@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource 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.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
@@ -150,7 +152,6 @@ fun PosterItem(
elevation: Dp = 8.dp, elevation: Dp = 8.dp,
contentDescription: String? contentDescription: String?
) { ) {
val context = LocalContext.current
Card( Card(
elevation = elevation, elevation = elevation,
modifier = modifier modifier = modifier
@@ -161,24 +162,26 @@ fun PosterItem(
AsyncImage( AsyncImage(
modifier = Modifier modifier = Modifier
.size(width = width, height = height) .size(width = width, height = height)
.clip(RoundedCornerShape(5f.dpToPx(context))) .clip(RoundedCornerShape(5.dp))
.clickable( .clickable(
onClick = onClick onClick = onClick
), ),
model = url, model = url,
placeholder = painterResource(id = placeholder), placeholder = rememberAsyncImagePainter(model = placeholder),
contentDescription = contentDescription contentDescription = contentDescription,
contentScale = ContentScale.FillBounds
) )
} else { } else {
Image( Image(
modifier = Modifier modifier = Modifier
.size(width = width, height = height) .size(width = width, height = height)
.clip(RoundedCornerShape(5f.dpToPx(context))) .clip(RoundedCornerShape(5.dp))
.clickable( .clickable(
onClick = onClick onClick = onClick
), ),
painter = painterResource(id = noDataImage), painter = rememberAsyncImagePainter(model = noDataImage),
contentDescription = contentDescription contentDescription = contentDescription,
contentScale = ContentScale.FillBounds
) )
} }
} }
@@ -212,7 +215,7 @@ fun BackdropImage(
val backdrop = collection.backdrops[page] val backdrop = collection.backdrops[page]
AsyncImage( AsyncImage(
model = TmdbUtils.getFullBackdropPath(backdrop), model = TmdbUtils.getFullBackdropPath(backdrop),
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 }
) )
@@ -221,15 +224,17 @@ fun BackdropImage(
if (imageUrl != null) { if (imageUrl != null) {
AsyncImage( AsyncImage(
model = imageUrl, model = imageUrl,
placeholder = painterResource(id = R.drawable.placeholder), placeholder = rememberAsyncImagePainter(model = R.drawable.placeholder),
contentDescription = contentDescription, contentDescription = contentDescription,
modifier = Modifier.onGloballyPositioned { sizeImage = it.size } modifier = Modifier.onGloballyPositioned { sizeImage = it.size },
contentScale = ContentScale.FillWidth
) )
} else { } else {
Image( Image(
painter = painterResource(id = R.drawable.placeholder), painter = rememberAsyncImagePainter(model = R.drawable.placeholder),
contentDescription = contentDescription, contentDescription = contentDescription,
modifier = Modifier.onGloballyPositioned { sizeImage = it.size } modifier = Modifier.onGloballyPositioned { sizeImage = it.size },
contentScale = ContentScale.FillWidth
) )
} }
} }

View File

@@ -63,6 +63,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AuthorDetails 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", model = "https://img.youtube.com/vi/${key}/hqdefault.jpg",
contentDescription = "", contentDescription = "",
placeholder = painterResource(id = R.drawable.placeholder) placeholder = rememberAsyncImagePainter(model = R.drawable.placeholder)
) )
if (showFullscreenView.value) { if (showFullscreenView.value) {
@@ -900,3 +901,27 @@ fun LinkableText(
style = style 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()
}
}
}

View File

@@ -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 RatedTvEpisodes: AccountTabNavItem(R.string.nav_rated_episodes_title, "rated_episodes_route", MediaViewType.EPISODE, screenContent, { SessionManager.currentSession?.ratedTvEpisodes ?: emptyList() }, RatedEpisode::class)
// object Lists // object Lists
object FavoriteMovies: AccountTabNavItem(R.string.nav_favorite_movies_title, "favorite_movies_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession?.favoriteMovies ?: emptyList() }, FavoriteMovie::class) 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 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) object TvWatchlist: AccountTabNavItem(R.string.nav_tv_watchlist_title, "tv_watchlist_route", MediaViewType.TV, screenContent, { SessionManager.currentSession?.tvWatchlist ?: emptyList() }, WatchlistTvSeries::class)
} }

View File

@@ -127,7 +127,7 @@ private fun Backdrop(modifier: Modifier, imageUrl: String?, contentDescription:
BackdropImage( BackdropImage(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.height(280.dp), .height(230.dp),
imageUrl = TmdbUtils.getFullBackdropPath(imageUrl), imageUrl = TmdbUtils.getFullBackdropPath(imageUrl),
contentDescription = contentDescription contentDescription = contentDescription
// collection = images.value // collection = images.value

View File

@@ -2,9 +2,15 @@ package com.owenlejeune.tvtime.ui.screens
import android.content.Context import android.content.Context
import android.widget.Toast 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.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
@@ -13,6 +19,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
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.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext 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.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize
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 androidx.navigation.NavController import androidx.navigation.NavController
import com.owenlejeune.tvtime.R 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.DetailService
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService 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.extensions.listItems
import com.owenlejeune.tvtime.ui.components.* import com.owenlejeune.tvtime.ui.components.*
import com.owenlejeune.tvtime.ui.navigation.MainNavItem 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.RatingSelected
import com.owenlejeune.tvtime.ui.theme.WatchlistSelected
import com.owenlejeune.tvtime.ui.theme.actionButtonColor import com.owenlejeune.tvtime.ui.theme.actionButtonColor
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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject import org.json.JSONObject
import java.text.DecimalFormat import java.text.DecimalFormat
@@ -82,7 +90,8 @@ fun MediaDetailView(
Column( Column(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp),
// horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (type == MediaViewType.MOVIE) { if (type == MediaViewType.MOVIE) {
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService) MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
@@ -90,6 +99,8 @@ fun MediaDetailView(
MiscTvDetails(mediaItem = mediaItem, service as TvService) MiscTvDetails(mediaItem = mediaItem, service as TvService)
} }
ActionsView(itemId = itemId, type = type, service = service)
OverviewCard(itemId = itemId, mediaItem = mediaItem, service = service) OverviewCard(itemId = itemId, mediaItem = mediaItem, service = service)
CastCard(itemId = itemId, service = service, appNavController = appNavController) CastCard(itemId = itemId, service = service, appNavController = appNavController)
@@ -98,8 +109,6 @@ fun MediaDetailView(
VideosCard(itemId = itemId, service = service) VideosCard(itemId = itemId, service = service)
ActionsView(itemId = itemId, type = type, service = service)
ReviewsCard(itemId = itemId, service = service) ReviewsCard(itemId = itemId, service = service)
} }
@@ -162,7 +171,6 @@ private fun MiscDetails(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(horizontal = 8.dp)
) { ) {
Text(text = year, color = MaterialTheme.colorScheme.onBackground) Text(text = year, color = MaterialTheme.colorScheme.onBackground)
Text( Text(
@@ -197,26 +205,29 @@ private fun ActionsView(
val session = SessionManager.currentSession val session = SessionManager.currentSession
Row( Row(
modifier = modifier modifier = modifier
.fillMaxWidth(), .wrapContentSize(),
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
RateButton( RateButton(
modifier = Modifier.weight(1f),
itemId = itemId, itemId = itemId,
type = type, type = type,
service = service service = service
) )
if (session?.isAuthorized == true) { if (session?.isAuthorized == true) {
ActionButton( val accountService = AccountService()
modifier = Modifier.weight(1f), WatchlistButton(
text = stringResource(R.string.add_to_list_action_label), itemId = itemId,
onClick = { /*TODO*/ } type = type
) )
ActionButton( ListButton(
modifier = Modifier.weight(1f), itemId = itemId,
text = stringResource(R.string.favourite_label), type = type,
onClick = { /*TODO*/ } service = accountService
)
FavoriteButton(
itemId = itemId,
type = type
) )
} }
} }
@@ -224,15 +235,17 @@ private fun ActionsView(
} }
@Composable @Composable
private fun ActionButton(modifier: Modifier, text: String, onClick: () -> Unit) { private fun ActionButton(
Button( iconRes: Int,
modifier = modifier, contentDescription: String,
shape = RoundedCornerShape(10.dp), isSelected: Boolean,
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.tertiary), filledIconColor: Color,
onClick = onClick modifier: Modifier = Modifier,
unfilledIconColor: Color = MaterialTheme.colorScheme.background,
backgroundColor: Color = MaterialTheme.colorScheme.actionButtonColor,
onClick: () -> Unit = {}
) { ) {
Text(text = text) // TODO - refactor buttons here
}
} }
@Composable @Composable
@@ -255,72 +268,219 @@ private fun RateButton(
) )
} }
val showSessionDialog = remember { mutableStateOf(false) }
val showRatingDialog = 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) } val showSessionDialog = remember { mutableStateOf(false) }
CircleBackgroundColorImage( CircleBackgroundColorImage(
modifier = modifier.clickable( modifier = modifier.clickable(
onClick = { onClick = {
if (SessionManager.currentSession != null) { if (session == null) {
showRatingDialog.value = true
} else {
showSessionDialog.value = true showSessionDialog.value = true
} else {
// add to watchlsit
} }
} }
), ),
size = 40.dp, size = 40.dp,
backgroundColor = MaterialTheme.colorScheme.actionButtonColor, backgroundColor = MaterialTheme.colorScheme.actionButtonColor,
painter = painterResource(id = R.drawable.ic_rating_star), painter = painterResource(id = R.drawable.ic_add_to_list),
colorFilter = ColorFilter.tint(color = if (itemIsRated.value) RatingSelected else MaterialTheme.colorScheme.background), colorFilter = ColorFilter.tint(color = /*if (hasWatchlistedItem.value) WatchlistSelected else*/ MaterialTheme.colorScheme.background),
contentDescription = "" contentDescription = ""
) )
RatingDialog(showDialog = showRatingDialog, onValueConfirmed = { rating -> CreateSessionDialog(showDialog = showSessionDialog, onSessionReturned = {})
if (rating > 0f) {
postRating(context, rating, itemId, service, itemIsRated)
} else {
deleteRating(context, itemId, service, itemIsRated)
}
})
CreateSessionDialog(showDialog = showSessionDialog, onSessionReturned = {
showRatingDialog.value = it
})
} }
fun postRating(context: Context, rating: Float, itemId: Int, service: DetailService, itemIsRated: MutableState<Boolean>) { @OptIn(ExperimentalAnimationApi::class)
CoroutineScope(Dispatchers.IO).launch { @Composable
val response = service.postRating(itemId, RatingBody(rating = rating)) fun FavoriteButton(
if (response.isSuccessful) { itemId: Int,
SessionManager.currentSession?.refresh(changed = arrayOf(SessionManager.Session.Changed.RatedMovies, SessionManager.Session.Changed.RatedTv)) type: MediaViewType,
withContext(Dispatchers.Main) { modifier: Modifier = Modifier
itemIsRated.value = true ) {
} val context = LocalContext.current
val session = SessionManager.currentSession
val hasFavorited = remember {
mutableStateOf(
if (type == MediaViewType.MOVIE) {
session?.hasFavoritedMovie(itemId) == true
} else { } else {
withContext(Dispatchers.Main) { session?.hasFavoritedTvShow(itemId) == true
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>) { val showSessionDialog = remember { mutableStateOf(false) }
CoroutineScope(Dispatchers.IO).launch {
val response = service.deleteRating(itemId) val bgColor = MaterialTheme.colorScheme.background
if (response.isSuccessful) { val filledColor = FavoriteSelected
SessionManager.currentSession?.refresh(changed = arrayOf(SessionManager.Session.Changed.RatedMovies, SessionManager.Session.Changed.RatedTv)) val tintColor = remember { Animatable(if (hasFavorited.value) filledColor else bgColor) }
withContext(Dispatchers.Main) {
itemIsRated.value = false 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 { } else {
withContext(Dispatchers.Main) { addToFavorite(context, itemId, type, hasFavorited) { added ->
val errorObj = JSONObject(response.errorBody().toString()) coroutineScope.launch {
Toast.makeText(context, "Error: ${errorObj.getString("status_message")}", Toast.LENGTH_SHORT).show() 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
)
}
}
@Composable @Composable
private fun CreateSessionDialog(showDialog: MutableState<Boolean>, onSessionReturned: (Boolean) -> Unit) { private fun CreateSessionDialog(showDialog: MutableState<Boolean>, onSessionReturned: (Boolean) -> Unit) {
@@ -373,14 +533,14 @@ private fun CreateSessionDialog(showDialog: MutableState<Boolean>, onSessionRetu
} }
@Composable @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 { fun formatPosition(position: Float): String {
return DecimalFormat("#.#").format(position.toInt()*5/10f) return DecimalFormat("#.#").format(position.toInt()*5/10f)
} }
if (showDialog.value) { if (showDialog.value) {
var sliderPosition by remember { mutableStateOf(0f) } var sliderPosition by remember { mutableStateOf(rating) }
val formatted = formatPosition(sliderPosition).toFloat() val formatted = formatPosition(sliderPosition).toFloat()
AlertDialog( AlertDialog(
modifier = Modifier.wrapContentHeight(), modifier = Modifier.wrapContentHeight(),
@@ -885,3 +1045,97 @@ 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)
}
}
}

View File

@@ -69,7 +69,8 @@ private val LightColorPalette = lightColorScheme(
fun TVTimeTheme( fun TVTimeTheme(
isDarkTheme: Boolean = isSystemInDarkTheme(), isDarkTheme: Boolean = isSystemInDarkTheme(),
isDynamicColor: Boolean = true, isDynamicColor: Boolean = true,
content: @Composable () -> Unit) { content: @Composable () -> Unit
) {
val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when { val colorScheme = when {
dynamicColor && isDarkTheme -> { dynamicColor && isDarkTheme -> {

View File

@@ -140,6 +140,13 @@ object SessionManager: KoinComponent {
return ratedTvEpisodes.map { it.id }.contains(id) 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 { fun hasFavoritedMovie(id: Int): Boolean {
return favoriteMovies.map { it.id }.contains(id) return favoriteMovies.map { it.id }.contains(id)
} }
@@ -173,6 +180,9 @@ object SessionManager: KoinComponent {
companion object { companion object {
val All get() = values() val All get() = values()
val Rated get() = arrayOf(RatedMovies, RatedTv, RatedEpisodes)
val Favorites get() = arrayOf(FavoriteMovies, FavoriteTv)
val Watchlist get() = arrayOf(WatchlistMovies, WatchlistTv)
} }
} }
} }

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<corners android:radius="20dp" />
<solid android:color="@android:color/darker_gray" /> <solid android:color="@android:color/darker_gray" />
</shape> </shape>