new rating icon and delete functionality

This commit is contained in:
Owen LeJeune
2022-03-12 00:04:03 -05:00
parent de4f137df9
commit f74b07102d
11 changed files with 277 additions and 103 deletions

View File

@@ -40,8 +40,10 @@ import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.BrushPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
@@ -59,10 +61,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@@ -72,6 +71,7 @@ import coil.transform.CircleCropTransformation
import com.google.accompanist.flowlayout.FlowRow
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.model.AuthorDetails
import com.owenlejeune.tvtime.ui.theme.RatingSelected
import com.owenlejeune.tvtime.utils.TmdbUtils
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener
@@ -616,9 +616,9 @@ fun HtmlText(text: String, modifier: Modifier = Modifier, color: Color = Color.U
fun CircleBackgroundColorImage(
size: Dp,
backgroundColor: Color,
image: ImageVector,
painter: Painter,
modifier: Modifier = Modifier,
imageHeight: Dp? = null,
imageSize: DpSize? = null,
imageAlignment: Alignment = Alignment.Center,
contentDescription: String? = null,
colorFilter: ColorFilter? = null
@@ -629,10 +629,56 @@ fun CircleBackgroundColorImage(
.size(size)
.background(color = backgroundColor)
) {
val mod = if (imageHeight != null) {
val mod = if (imageSize != null) {
Modifier
.align(imageAlignment)
.height(height = imageHeight)
.size(size = imageSize)
} else {
Modifier.align(imageAlignment)
}
Image(
contentDescription = contentDescription,
modifier = mod,
colorFilter = colorFilter,
painter = painter,
contentScale = ContentScale.FillBounds
)
}
}
@Composable
@Preview
private fun CircleBackgroundColorImagePreview() {
CircleBackgroundColorImage(
size = 100.dp,
backgroundColor = MaterialTheme.colorScheme.inverseSurface,
painter = painterResource(id = R.drawable.ic_rating_star),
colorFilter = ColorFilter.tint(color = RatingSelected),
imageSize = DpSize(width = 70.dp, height = 70.dp)
)
}
@Composable
fun CircleBackgroundColorImage(
size: Dp,
backgroundColor: Color,
image: ImageVector,
modifier: Modifier = Modifier,
imageSize: DpSize? = null,
imageAlignment: Alignment = Alignment.Center,
contentDescription: String? = null,
colorFilter: ColorFilter? = null
) {
Box(
modifier = modifier
.clip(CircleShape)
.size(size)
.background(color = backgroundColor)
) {
val mod = if (imageSize != null) {
Modifier
.align(imageAlignment)
.size(size = imageSize)
} else {
Modifier.align(imageAlignment)
}

View File

@@ -1,13 +1,11 @@
package com.owenlejeune.tvtime.ui.screens
import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Send
@@ -18,10 +16,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
@@ -33,6 +33,8 @@ import com.owenlejeune.tvtime.api.tmdb.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.RatingSelected
import com.owenlejeune.tvtime.ui.theme.actionButtonColor
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.TmdbUtils
import kotlinx.coroutines.CoroutineScope
@@ -243,7 +245,7 @@ private fun RateButton(
val session = SessionManager.currentSession
val context = LocalContext.current
var itemIsRated by remember {
val itemIsRated = remember {
mutableStateOf(
if (type == MediaViewType.MOVIE) {
session?.hasRatedMovie(itemId) == true
@@ -255,43 +257,29 @@ private fun RateButton(
val showRatingDialog = remember { mutableStateOf(false) }
val showSessionDialog = remember { mutableStateOf(false) }
ActionButton(
modifier = modifier,
text = if (itemIsRated) stringResource(R.string.delete_rating_action_label) else stringResource(R.string.rate_action_label),
onClick = {
if (!itemIsRated) {
CircleBackgroundColorImage(
modifier = Modifier.clickable(
onClick = {
if (SessionManager.currentSession != null) {
showRatingDialog.value = true
} else {
showSessionDialog.value = true
}
} else {
CoroutineScope(Dispatchers.IO).launch {
val response = service.deleteRating(itemId)
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
itemIsRated = false
}
}
SessionManager.currentSession?.refresh()
}
}
}
),
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),
contentDescription = ""
)
RatingDialog(showDialog = showRatingDialog, onValueConfirmed = { rating ->
CoroutineScope(Dispatchers.IO).launch {
val response = service.postRating(itemId, RatingBody(rating = rating))
if (response.isSuccessful) {
SessionManager.currentSession?.refresh()
withContext(Dispatchers.Main) {
itemIsRated = true
}
} else {
withContext(Dispatchers.Main) {
val errorObj = JSONObject(response.errorBody().toString())
Toast.makeText(context, "Error: ${errorObj.getString("status_message")}", Toast.LENGTH_SHORT).show()
}
}
if (rating > 0f) {
postRating(context, rating, itemId, service, itemIsRated)
} else {
deleteRating(context, itemId, service, itemIsRated)
}
})
@@ -300,6 +288,40 @@ private fun RateButton(
})
}
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()
}
}
}
}
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()
}
}
}
}
@Composable
private fun CreateSessionDialog(showDialog: MutableState<Boolean>, onSessionReturned: (Boolean) -> Unit) {
if (showDialog.value) {
@@ -359,6 +381,7 @@ private fun RatingDialog(showDialog: MutableState<Boolean>, onValueConfirmed: (F
if (showDialog.value) {
var sliderPosition by remember { mutableStateOf(0f) }
val formatted = formatPosition(sliderPosition).toFloat()
AlertDialog(
modifier = Modifier.wrapContentHeight(),
onDismissRequest = { showDialog.value = false },
@@ -367,11 +390,17 @@ private fun RatingDialog(showDialog: MutableState<Boolean>, onValueConfirmed: (F
Button(
modifier = Modifier.height(40.dp),
onClick = {
onValueConfirmed.invoke(formatPosition(sliderPosition).toFloat())
onValueConfirmed.invoke(formatted)
showDialog.value = false
}
) {
Text(stringResource(R.string.rating_dialog_confirm))
Text(
text = if (formatted > 0f) {
stringResource(id = R.string.rating_dialog_confirm)
} else {
stringResource(id = R.string.rating_dialog_delete)
}
)
}
},
dismissButton = {
@@ -702,7 +731,7 @@ private fun ReviewsCard(
size = 30.dp,
backgroundColor = MaterialTheme.colorScheme.error,
contentDescription = "",
imageHeight = 15.dp,
imageSize = DpSize(width = 20.dp, height = 15.dp),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.surfaceVariant)
)
}

View File

@@ -122,7 +122,9 @@ fun <T: Any> AccountTabContent(
LazyColumn(modifier = Modifier
.fillMaxSize()
.padding(12.dp)) {
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (contentItems.isEmpty()) {
item {
Text(

View File

@@ -48,4 +48,12 @@ val OnErrorDark = Color(0xFF601410)
val ErrorContainerLight = Color(0xfff9dedc)
val ErrorContainerDark = Color(0xff8c1d18)
val OnErrorContainerLight = Color(0xff410e0b)
val OnErrorContainerDark = Color(0xfff2b8b5)
val OnErrorContainerDark = Color(0xfff2b8b5)
val ActionUnselected = Color(0xFFFFFFFF)
val WatchlistSelected = Color(0xFFBF3F39)
val RatingSelected = Color(0xFFE7C343)
val FavoriteSelected = Color(0xFFDD57B2)
val LightActionButton = Color(0xFF48C9B0)
val DarkActionButton = Color(0xFF17A589)

View File

@@ -0,0 +1,15 @@
package com.owenlejeune.tvtime.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
val ColorScheme.actionButtonColor: Color
@Composable
get() = get(LightActionButton, DarkActionButton)
@Composable
private fun get(light: Color, dark: Color): Color {
return if(isSystemInDarkTheme()) dark else light
}

View File

@@ -33,7 +33,7 @@ private val DarkColorPalette = darkColorScheme(
surfaceVariant = N2_700,
onSurfaceVariant = N2_200,
inverseSurface = N1_100,
inverseOnSurface = N1_900
inverseOnSurface = N1_900,
)
private val LightColorPalette = lightColorScheme(

View File

@@ -158,7 +158,23 @@ object SessionManager: KoinComponent {
abstract suspend fun initialize()
abstract suspend fun refresh()
abstract suspend fun refresh(changed: Array<Changed> = Changed.All)
enum class Changed {
AccountDetails,
Lists,
RatedMovies,
RatedTv,
RatedEpisodes,
FavoriteMovies,
FavoriteTv,
WatchlistMovies,
WatchlistTv;
companion object {
val All get() = values()
}
}
}
private class AuthorizedSession: Session(preferences.authorizedSessionId, true) {
@@ -168,75 +184,95 @@ object SessionManager: KoinComponent {
refresh()
}
override suspend fun refresh() {
service.getAccountDetails().apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_accountDetails = body() ?: _accountDetails
accountDetails?.let {
CoroutineScope(Dispatchers.IO).launch {
refreshWithAccountId(it.id)
override suspend fun refresh(changed: Array<Changed>) {
if (changed.contains(Changed.AccountDetails)) {
service.getAccountDetails().apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_accountDetails = body() ?: _accountDetails
accountDetails?.let {
CoroutineScope(Dispatchers.IO).launch {
refreshWithAccountId(it.id, changed)
}
}
}
}
}
} else if (accountDetails != null) {
refreshWithAccountId(accountDetails!!.id, changed)
}
}
private suspend fun refreshWithAccountId(accountId: Int) {
service.getLists(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_accountLists = body()?.results ?: _accountLists
private suspend fun refreshWithAccountId(accountId: Int, changed: Array<Changed> = Changed.All) {
if (changed.contains(Changed.Lists)) {
service.getLists(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_accountLists = body()?.results ?: _accountLists
}
}
}
}
service.getFavoriteMovies(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_favoriteMovies = body()?.results ?: _favoriteMovies
if (changed.contains(Changed.FavoriteMovies)) {
service.getFavoriteMovies(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_favoriteMovies = body()?.results ?: _favoriteMovies
}
}
}
}
service.getFavoriteTvShows(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_favoriteTvShows = body()?.results ?: _favoriteTvShows
if (changed.contains(Changed.FavoriteTv)) {
service.getFavoriteTvShows(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_favoriteTvShows = body()?.results ?: _favoriteTvShows
}
}
}
}
service.getRatedMovies(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_ratedMovies = body()?.results ?: _ratedMovies
if (changed.contains(Changed.RatedMovies)) {
service.getRatedMovies(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_ratedMovies = body()?.results ?: _ratedMovies
}
}
}
}
service.getRatedTvShows(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_ratedTvShows = body()?.results ?: _ratedTvShows
if (changed.contains(Changed.RatedTv)) {
service.getRatedTvShows(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_ratedTvShows = body()?.results ?: _ratedTvShows
}
}
}
}
service.getRatedTvEpisodes(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_ratedTvEpisodes = body()?.results ?: _ratedTvEpisodes
if (changed.contains(Changed.RatedEpisodes)) {
service.getRatedTvEpisodes(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_ratedTvEpisodes = body()?.results ?: _ratedTvEpisodes
}
}
}
}
service.getMovieWatchlist(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_movieWatchlist = body()?.results ?: _movieWatchlist
if (changed.contains(Changed.WatchlistMovies)) {
service.getMovieWatchlist(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_movieWatchlist = body()?.results ?: _movieWatchlist
}
}
}
}
service.getTvWatchlist(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_tvWatchlist = body()?.results ?: _tvWatchlist
if (changed.contains(Changed.WatchlistTv)) {
service.getTvWatchlist(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_tvWatchlist = body()?.results ?: _tvWatchlist
}
}
}
}
@@ -250,25 +286,31 @@ object SessionManager: KoinComponent {
refresh()
}
override suspend fun refresh() {
service.getRatedMovies(sessionId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_ratedMovies = body()?.results ?: _ratedMovies
override suspend fun refresh(changed: Array<Changed>) {
if (changed.contains(Changed.RatedMovies)) {
service.getRatedMovies(sessionId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_ratedMovies = body()?.results ?: _ratedMovies
}
}
}
}
service.getRatedTvShows(sessionId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_ratedTvShows = body()?.results ?: _ratedTvShows
if (changed.contains(Changed.RatedTv)) {
service.getRatedTvShows(sessionId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_ratedTvShows = body()?.results ?: _ratedTvShows
}
}
}
}
service.getRatedTvEpisodes(sessionId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_ratedTvEpisodes = body()?.results ?: _ratedTvEpisodes
if (changed.contains(Changed.RatedEpisodes)) {
service.getRatedTvEpisodes(sessionId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
_ratedTvEpisodes = body()?.results ?: _ratedTvEpisodes
}
}
}
}

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M4,10.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM4,4.5c-0.83,0 -1.5,0.67 -1.5,1.5S3.17,7.5 4,7.5 5.5,6.83 5.5,6 4.83,4.5 4,4.5zM4,16.5c-0.83,0 -1.5,0.68 -1.5,1.5s0.68,1.5 1.5,1.5 1.5,-0.68 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM7,19h14v-2L7,17v2zM7,13h14v-2L7,11v2zM7,5v2h14L21,5L7,5z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@@ -67,6 +67,7 @@
<string name="rating_dialog_title">Add a Rating</string>
<string name="rating_dialog_confirm">Submit rating</string>
<string name="rating_dialog_delete">Delete rating</string>
<string name="action_cancel">Cancel</string>