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.geometry.Offset
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.BrushPainter 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.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource 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.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
@@ -72,6 +71,7 @@ import coil.transform.CircleCropTransformation
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.model.AuthorDetails import com.owenlejeune.tvtime.api.tmdb.model.AuthorDetails
import com.owenlejeune.tvtime.ui.theme.RatingSelected
import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.utils.TmdbUtils
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener 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( fun CircleBackgroundColorImage(
size: Dp, size: Dp,
backgroundColor: Color, backgroundColor: Color,
image: ImageVector, painter: Painter,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
imageHeight: Dp? = null, imageSize: DpSize? = null,
imageAlignment: Alignment = Alignment.Center, imageAlignment: Alignment = Alignment.Center,
contentDescription: String? = null, contentDescription: String? = null,
colorFilter: ColorFilter? = null colorFilter: ColorFilter? = null
@@ -629,10 +629,56 @@ fun CircleBackgroundColorImage(
.size(size) .size(size)
.background(color = backgroundColor) .background(color = backgroundColor)
) { ) {
val mod = if (imageHeight != null) { val mod = if (imageSize != null) {
Modifier Modifier
.align(imageAlignment) .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 { } else {
Modifier.align(imageAlignment) Modifier.align(imageAlignment)
} }

View File

@@ -1,13 +1,11 @@
package com.owenlejeune.tvtime.ui.screens package com.owenlejeune.tvtime.ui.screens
import android.content.Context
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.*
import androidx.compose.foundation.background
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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
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
import androidx.compose.material.icons.filled.Send 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.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
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle 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.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
@@ -33,6 +33,8 @@ import com.owenlejeune.tvtime.api.tmdb.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.RatingSelected
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.CoroutineScope
@@ -243,7 +245,7 @@ private fun RateButton(
val session = SessionManager.currentSession val session = SessionManager.currentSession
val context = LocalContext.current val context = LocalContext.current
var itemIsRated by remember { val itemIsRated = remember {
mutableStateOf( mutableStateOf(
if (type == MediaViewType.MOVIE) { if (type == MediaViewType.MOVIE) {
session?.hasRatedMovie(itemId) == true session?.hasRatedMovie(itemId) == true
@@ -255,36 +257,44 @@ private fun RateButton(
val showRatingDialog = remember { mutableStateOf(false) } val showRatingDialog = remember { mutableStateOf(false) }
val showSessionDialog = remember { mutableStateOf(false) } val showSessionDialog = remember { mutableStateOf(false) }
ActionButton(
modifier = modifier, CircleBackgroundColorImage(
text = if (itemIsRated) stringResource(R.string.delete_rating_action_label) else stringResource(R.string.rate_action_label), modifier = Modifier.clickable(
onClick = { onClick = {
if (!itemIsRated) {
if (SessionManager.currentSession != null) { if (SessionManager.currentSession != null) {
showRatingDialog.value = true showRatingDialog.value = true
} else { } else {
showSessionDialog.value = true 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 -> 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
})
}
fun postRating(context: Context, rating: Float, itemId: Int, service: DetailService, itemIsRated: MutableState<Boolean>) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val response = service.postRating(itemId, RatingBody(rating = rating)) val response = service.postRating(itemId, RatingBody(rating = rating))
if (response.isSuccessful) { if (response.isSuccessful) {
SessionManager.currentSession?.refresh() SessionManager.currentSession?.refresh(changed = arrayOf(SessionManager.Session.Changed.RatedMovies, SessionManager.Session.Changed.RatedTv))
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
itemIsRated = true itemIsRated.value = true
} }
} else { } else {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -293,11 +303,23 @@ private fun RateButton(
} }
} }
} }
}) }
CreateSessionDialog(showDialog = showSessionDialog, onSessionReturned = { fun deleteRating(context: Context, itemId: Int, service: DetailService, itemIsRated: MutableState<Boolean>) {
showRatingDialog.value = it 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 @Composable
@@ -359,6 +381,7 @@ private fun RatingDialog(showDialog: MutableState<Boolean>, onValueConfirmed: (F
if (showDialog.value) { if (showDialog.value) {
var sliderPosition by remember { mutableStateOf(0f) } var sliderPosition by remember { mutableStateOf(0f) }
val formatted = formatPosition(sliderPosition).toFloat()
AlertDialog( AlertDialog(
modifier = Modifier.wrapContentHeight(), modifier = Modifier.wrapContentHeight(),
onDismissRequest = { showDialog.value = false }, onDismissRequest = { showDialog.value = false },
@@ -367,11 +390,17 @@ private fun RatingDialog(showDialog: MutableState<Boolean>, onValueConfirmed: (F
Button( Button(
modifier = Modifier.height(40.dp), modifier = Modifier.height(40.dp),
onClick = { onClick = {
onValueConfirmed.invoke(formatPosition(sliderPosition).toFloat()) onValueConfirmed.invoke(formatted)
showDialog.value = false 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 = { dismissButton = {
@@ -702,7 +731,7 @@ private fun ReviewsCard(
size = 30.dp, size = 30.dp,
backgroundColor = MaterialTheme.colorScheme.error, backgroundColor = MaterialTheme.colorScheme.error,
contentDescription = "", contentDescription = "",
imageHeight = 15.dp, imageSize = DpSize(width = 20.dp, height = 15.dp),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.surfaceVariant) colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.surfaceVariant)
) )
} }

View File

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

View File

@@ -49,3 +49,11 @@ val ErrorContainerLight = Color(0xfff9dedc)
val ErrorContainerDark = Color(0xff8c1d18) val ErrorContainerDark = Color(0xff8c1d18)
val OnErrorContainerLight = Color(0xff410e0b) 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, surfaceVariant = N2_700,
onSurfaceVariant = N2_200, onSurfaceVariant = N2_200,
inverseSurface = N1_100, inverseSurface = N1_100,
inverseOnSurface = N1_900 inverseOnSurface = N1_900,
) )
private val LightColorPalette = lightColorScheme( private val LightColorPalette = lightColorScheme(

View File

@@ -158,7 +158,23 @@ object SessionManager: KoinComponent {
abstract suspend fun initialize() 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) { private class AuthorizedSession: Session(preferences.authorizedSessionId, true) {
@@ -168,22 +184,27 @@ object SessionManager: KoinComponent {
refresh() refresh()
} }
override suspend fun refresh() { override suspend fun refresh(changed: Array<Changed>) {
if (changed.contains(Changed.AccountDetails)) {
service.getAccountDetails().apply { service.getAccountDetails().apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_accountDetails = body() ?: _accountDetails _accountDetails = body() ?: _accountDetails
accountDetails?.let { accountDetails?.let {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
refreshWithAccountId(it.id) refreshWithAccountId(it.id, changed)
} }
} }
} }
} }
} }
} else if (accountDetails != null) {
refreshWithAccountId(accountDetails!!.id, changed)
}
} }
private suspend fun refreshWithAccountId(accountId: Int) { private suspend fun refreshWithAccountId(accountId: Int, changed: Array<Changed> = Changed.All) {
if (changed.contains(Changed.Lists)) {
service.getLists(accountId).apply { service.getLists(accountId).apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -191,6 +212,8 @@ object SessionManager: KoinComponent {
} }
} }
} }
}
if (changed.contains(Changed.FavoriteMovies)) {
service.getFavoriteMovies(accountId).apply { service.getFavoriteMovies(accountId).apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -198,6 +221,8 @@ object SessionManager: KoinComponent {
} }
} }
} }
}
if (changed.contains(Changed.FavoriteTv)) {
service.getFavoriteTvShows(accountId).apply { service.getFavoriteTvShows(accountId).apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -205,6 +230,8 @@ object SessionManager: KoinComponent {
} }
} }
} }
}
if (changed.contains(Changed.RatedMovies)) {
service.getRatedMovies(accountId).apply { service.getRatedMovies(accountId).apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -212,6 +239,8 @@ object SessionManager: KoinComponent {
} }
} }
} }
}
if (changed.contains(Changed.RatedTv)) {
service.getRatedTvShows(accountId).apply { service.getRatedTvShows(accountId).apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -219,6 +248,8 @@ object SessionManager: KoinComponent {
} }
} }
} }
}
if (changed.contains(Changed.RatedEpisodes)) {
service.getRatedTvEpisodes(accountId).apply { service.getRatedTvEpisodes(accountId).apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -226,6 +257,8 @@ object SessionManager: KoinComponent {
} }
} }
} }
}
if (changed.contains(Changed.WatchlistMovies)) {
service.getMovieWatchlist(accountId).apply { service.getMovieWatchlist(accountId).apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -233,6 +266,8 @@ object SessionManager: KoinComponent {
} }
} }
} }
}
if (changed.contains(Changed.WatchlistTv)) {
service.getTvWatchlist(accountId).apply { service.getTvWatchlist(accountId).apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -242,6 +277,7 @@ object SessionManager: KoinComponent {
} }
} }
} }
}
private class GuestSession: Session(preferences.guestSessionId, false) { private class GuestSession: Session(preferences.guestSessionId, false) {
private val service by lazy { GuestSessionService() } private val service by lazy { GuestSessionService() }
@@ -250,7 +286,8 @@ object SessionManager: KoinComponent {
refresh() refresh()
} }
override suspend fun refresh() { override suspend fun refresh(changed: Array<Changed>) {
if (changed.contains(Changed.RatedMovies)) {
service.getRatedMovies(sessionId).apply { service.getRatedMovies(sessionId).apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -258,6 +295,8 @@ object SessionManager: KoinComponent {
} }
} }
} }
}
if (changed.contains(Changed.RatedTv)) {
service.getRatedTvShows(sessionId).apply { service.getRatedTvShows(sessionId).apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -265,6 +304,8 @@ object SessionManager: KoinComponent {
} }
} }
} }
}
if (changed.contains(Changed.RatedEpisodes)) {
service.getRatedTvEpisodes(sessionId).apply { service.getRatedTvEpisodes(sessionId).apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -274,5 +315,6 @@ object SessionManager: KoinComponent {
} }
} }
} }
}
} }

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_title">Add a Rating</string>
<string name="rating_dialog_confirm">Submit rating</string> <string name="rating_dialog_confirm">Submit rating</string>
<string name="rating_dialog_delete">Delete rating</string>
<string name="action_cancel">Cancel</string> <string name="action_cancel">Cancel</string>