mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-19 02:00:54 -05:00
basis for session manager and adding rating
This commit is contained in:
@@ -13,11 +13,20 @@ import androidx.navigation.compose.rememberNavController
|
|||||||
import com.owenlejeune.tvtime.ui.navigation.MainNavigationRoutes
|
import com.owenlejeune.tvtime.ui.navigation.MainNavigationRoutes
|
||||||
import com.owenlejeune.tvtime.ui.theme.TVTimeTheme
|
import com.owenlejeune.tvtime.ui.theme.TVTimeTheme
|
||||||
import com.owenlejeune.tvtime.utils.KeyboardManager
|
import com.owenlejeune.tvtime.utils.KeyboardManager
|
||||||
|
import com.owenlejeune.tvtime.utils.SessionManager
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
SessionManager.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
AppKeyboardFocusManager()
|
AppKeyboardFocusManager()
|
||||||
val displayUnderStatusBar = remember { mutableStateOf(false) }
|
val displayUnderStatusBar = remember { mutableStateOf(false) }
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb
|
||||||
|
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionBody
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionResponse
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.GuestSessionResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.DELETE
|
||||||
|
import retrofit2.http.GET
|
||||||
|
|
||||||
|
interface AuthenticationApi {
|
||||||
|
|
||||||
|
@GET("authentication/guest_session/new")
|
||||||
|
suspend fun getNewGuestSession(): Response<GuestSessionResponse>
|
||||||
|
|
||||||
|
@DELETE("authentication/session")
|
||||||
|
suspend fun deleteSession(@Body body: DeleteSessionBody): Response<DeleteSessionResponse>
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb
|
||||||
|
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionBody
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionResponse
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.GuestSessionResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
class AuthenticationService {
|
||||||
|
|
||||||
|
private val service by lazy { TmdbClient().createAuthenticationService() }
|
||||||
|
|
||||||
|
suspend fun getNewGuestSession(): Response<GuestSessionResponse> {
|
||||||
|
return service.getNewGuestSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteSession(body: DeleteSessionBody): Response<DeleteSessionResponse> {
|
||||||
|
return service.deleteSession(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb
|
||||||
|
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.RatedMediaResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
interface GuestSessionApi {
|
||||||
|
|
||||||
|
@GET("guest_session/{session_id}/rated/movies")
|
||||||
|
suspend fun getRatedMovies(@Path("session_id") sessionId: String): Response<RatedMediaResponse>
|
||||||
|
|
||||||
|
@GET("guest_session/{session_id}/rated/tv")
|
||||||
|
suspend fun getRatedTvShows(@Path("session_id") sessionId: String): Response<RatedMediaResponse>
|
||||||
|
|
||||||
|
@GET("guest_session/{session_id}/rated/tv/episodes")
|
||||||
|
suspend fun getRatedTvEpisodes(@Path("session_id") sessionId: String): Response<RatedMediaResponse>
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb
|
||||||
|
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.RatedMedia
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.RatedMediaResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
class GuestSessionService {
|
||||||
|
|
||||||
|
private val service by lazy { TmdbClient().createGuestSessionService() }
|
||||||
|
|
||||||
|
suspend fun getRatedMovies(sessionId: String): Response<RatedMediaResponse> {
|
||||||
|
return service.getRatedMovies(sessionId = sessionId).apply {
|
||||||
|
body()?.results?.forEach { it.type = RatedMedia.Type.MOVIE }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getRatedTvShows(sessionId: String): Response<RatedMediaResponse> {
|
||||||
|
return service.getRatedTvShows(sessionId = sessionId).apply {
|
||||||
|
body()?.results?.forEach { it.type = RatedMedia.Type.SERIES }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getRatedTvEpisodes(sessionId: String): Response<RatedMediaResponse> {
|
||||||
|
return service.getRatedTvEpisodes(sessionId = sessionId).apply {
|
||||||
|
body()?.results?.forEach { it.type = RatedMedia.Type.EPISODE }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,9 +2,7 @@ package com.owenlejeune.tvtime.api.tmdb
|
|||||||
|
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.*
|
||||||
import retrofit2.http.Path
|
|
||||||
import retrofit2.http.Query
|
|
||||||
|
|
||||||
interface MoviesApi {
|
interface MoviesApi {
|
||||||
|
|
||||||
@@ -41,4 +39,30 @@ interface MoviesApi {
|
|||||||
@GET("movie/{id}/reviews")
|
@GET("movie/{id}/reviews")
|
||||||
suspend fun getReviews(@Path("id") id: Int): Response<ReviewResponse>
|
suspend fun getReviews(@Path("id") id: Int): Response<ReviewResponse>
|
||||||
|
|
||||||
|
@POST("movie/{id}/rating")
|
||||||
|
suspend fun postMovieRatingAsGuest(
|
||||||
|
@Path("id") id: Int,
|
||||||
|
@Query("guest_session_id") guestSessionId: String,
|
||||||
|
@Body ratingBody: RatingBody
|
||||||
|
): Response<RatingResponse>
|
||||||
|
|
||||||
|
@POST("movie/{id}/rating")
|
||||||
|
suspend fun postMovieRatingAsUser(
|
||||||
|
@Path("id") id: Int,
|
||||||
|
@Query("session_id") sessionId: String,
|
||||||
|
@Body ratingBody: RatingBody
|
||||||
|
): Response<RatingResponse>
|
||||||
|
|
||||||
|
@DELETE("movie/{id}/rating")
|
||||||
|
suspend fun deleteMovieReviewAsGuest(
|
||||||
|
@Path("id") id: Int,
|
||||||
|
@Query("guest_session_id") guestSessionId: String
|
||||||
|
): Response<RatingResponse>
|
||||||
|
|
||||||
|
@DELETE("movie/{id}/rating")
|
||||||
|
suspend fun deleteMovieReviewAsUser(
|
||||||
|
@Path("id") id: Int,
|
||||||
|
@Query("session_id") sessionId: String
|
||||||
|
): Response<RatingResponse>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,55 +1,79 @@
|
|||||||
package com.owenlejeune.tvtime.api.tmdb
|
package com.owenlejeune.tvtime.api.tmdb
|
||||||
|
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||||
|
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||||
|
import com.owenlejeune.tvtime.utils.SessionManager
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
class MoviesService: KoinComponent, DetailService, HomePageService {
|
class MoviesService: KoinComponent, DetailService, HomePageService {
|
||||||
|
|
||||||
private val service by lazy { TmdbClient().createMovieService() }
|
private val preferences: AppPreferences by inject()
|
||||||
|
|
||||||
|
private val movieService by lazy { TmdbClient().createMovieService() }
|
||||||
|
private val authService by lazy { TmdbClient().createAuthenticationService() }
|
||||||
|
|
||||||
override suspend fun getPopular(page: Int): Response<out HomePageResponse> {
|
override suspend fun getPopular(page: Int): Response<out HomePageResponse> {
|
||||||
return service.getPopularMovies(page)
|
return movieService.getPopularMovies(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getNowPlaying(page: Int): Response<out HomePageResponse> {
|
override suspend fun getNowPlaying(page: Int): Response<out HomePageResponse> {
|
||||||
return service.getNowPlayingMovies(page)
|
return movieService.getNowPlayingMovies(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getTopRated(page: Int): Response<out HomePageResponse> {
|
override suspend fun getTopRated(page: Int): Response<out HomePageResponse> {
|
||||||
return service.getTopRatedMovies(page)
|
return movieService.getTopRatedMovies(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUpcoming(page: Int): Response<out HomePageResponse> {
|
override suspend fun getUpcoming(page: Int): Response<out HomePageResponse> {
|
||||||
return service.getUpcomingMovies(page)
|
return movieService.getUpcomingMovies(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getReleaseDates(id: Int): Response<MovieReleaseResults> {
|
suspend fun getReleaseDates(id: Int): Response<MovieReleaseResults> {
|
||||||
return service.getReleaseDates(id)
|
return movieService.getReleaseDates(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getById(id: Int): Response<out DetailedItem> {
|
override suspend fun getById(id: Int): Response<out DetailedItem> {
|
||||||
return service.getMovieById(id)
|
return movieService.getMovieById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getImages(id: Int): Response<ImageCollection> {
|
override suspend fun getImages(id: Int): Response<ImageCollection> {
|
||||||
return service.getMovieImages(id)
|
return movieService.getMovieImages(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getCastAndCrew(id: Int): Response<CastAndCrew> {
|
override suspend fun getCastAndCrew(id: Int): Response<CastAndCrew> {
|
||||||
return service.getCastAndCrew(id)
|
return movieService.getCastAndCrew(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> {
|
override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> {
|
||||||
return service.getSimilarMovies(id, page)
|
return movieService.getSimilarMovies(id, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getVideos(id: Int): Response<VideoResponse> {
|
override suspend fun getVideos(id: Int): Response<VideoResponse> {
|
||||||
return service.getVideos(id)
|
return movieService.getVideos(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getReviews(id: Int): Response<ReviewResponse> {
|
override suspend fun getReviews(id: Int): Response<ReviewResponse> {
|
||||||
return service.getReviews(id)
|
return movieService.getReviews(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> {
|
||||||
|
val session = SessionManager.currentSession
|
||||||
|
return if (session.isGuest) {
|
||||||
|
movieService.postMovieRatingAsGuest(id, session.sessionId, rating)
|
||||||
|
} else {
|
||||||
|
movieService.postMovieRatingAsUser(id, session.sessionId, rating)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteRating(id: Int, rating: RatingBody): Response<RatingResponse> {
|
||||||
|
val session = SessionManager.currentSession
|
||||||
|
return if (session.isGuest) {
|
||||||
|
movieService.deleteMovieReviewAsGuest(id, session.sessionId)
|
||||||
|
} else {
|
||||||
|
movieService.deleteMovieReviewAsUser(id, session.sessionId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,14 @@ class TmdbClient: KoinComponent {
|
|||||||
return client.create(PeopleApi::class.java)
|
return client.create(PeopleApi::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createAuthenticationService(): AuthenticationApi {
|
||||||
|
return client.create(AuthenticationApi::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createGuestSessionService(): GuestSessionApi {
|
||||||
|
return client.create(GuestSessionApi::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private inner class TmdbInterceptor: Interceptor {
|
private inner class TmdbInterceptor: Interceptor {
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val apiParam = QueryParam("api_key", BuildConfig.TMDB_ApiKey)
|
val apiParam = QueryParam("api_key", BuildConfig.TMDB_ApiKey)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
class DeleteSessionBody(
|
||||||
|
@SerializedName("session_id") val sessionsId: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
class DeleteSessionResponse(
|
||||||
|
@SerializedName("success") val success: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
class GuestSessionResponse(
|
||||||
|
@SerializedName("success") val success: Boolean,
|
||||||
|
@SerializedName("guest_session_id") val guestSessionId: String,
|
||||||
|
@SerializedName("expires_at") val expiry: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
class RatedMedia(
|
||||||
|
@SerializedName("id") val id: Int,
|
||||||
|
var type: Type
|
||||||
|
) {
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
MOVIE,
|
||||||
|
SERIES,
|
||||||
|
EPISODE
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
class RatedMediaResponse(
|
||||||
|
@SerializedName("results") val results: List<RatedMedia>
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
class RatingBody(
|
||||||
|
@SerializedName("value") val rating: Float
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
class RatingResponse(
|
||||||
|
@SerializedName("status_code") val statusCode: Int,
|
||||||
|
@SerializedName("status_message") val statusMessage: String
|
||||||
|
)
|
||||||
@@ -11,6 +11,7 @@ class AppPreferences(context: Context) {
|
|||||||
// private val USE_PREFERENCES = "use_android_12_colors"
|
// private val USE_PREFERENCES = "use_android_12_colors"
|
||||||
private val PERSISTENT_SEARCH = "persistent_search"
|
private val PERSISTENT_SEARCH = "persistent_search"
|
||||||
private val HIDE_TITLE = "hide_title"
|
private val HIDE_TITLE = "hide_title"
|
||||||
|
private val GUEST_SESSION = "guest_session_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE)
|
private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE)
|
||||||
@@ -22,6 +23,10 @@ class AppPreferences(context: Context) {
|
|||||||
var hideTitle: Boolean
|
var hideTitle: Boolean
|
||||||
get() = preferences.getBoolean(HIDE_TITLE, false)
|
get() = preferences.getBoolean(HIDE_TITLE, false)
|
||||||
set(value) { preferences.put(HIDE_TITLE, value) }
|
set(value) { preferences.put(HIDE_TITLE, value) }
|
||||||
|
|
||||||
|
var guestSessionId: String
|
||||||
|
get() = preferences.getString(GUEST_SESSION, "") ?: ""
|
||||||
|
set(value) { preferences.put(GUEST_SESSION, value) }
|
||||||
// val usePreferences: MutableState<Boolean>
|
// val usePreferences: MutableState<Boolean>
|
||||||
// var usePreferences: Boolean
|
// var usePreferences: Boolean
|
||||||
// get() = preferences.getBoolean(USE_PREFERENCES, false)
|
// get() = preferences.getBoolean(USE_PREFERENCES, false)
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.owenlejeune.tvtime.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Slider
|
||||||
|
import androidx.compose.material.SliderDefaults
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SliderWithLabel(
|
||||||
|
value: Float,
|
||||||
|
valueRange: ClosedFloatingPointRange<Float>,
|
||||||
|
onValueChanged: (Float) -> Unit,
|
||||||
|
sliderLabel: String,
|
||||||
|
step: Int = 0,
|
||||||
|
labelMinWidth: Dp = 24.dp
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
|
||||||
|
val offset = getSliderOffset(
|
||||||
|
value = value,
|
||||||
|
valueRange = valueRange,
|
||||||
|
boxWidth = maxWidth,
|
||||||
|
labelWidth = labelMinWidth + 8.dp // Since we use a padding of 4.dp on either sides of the SliderLabel, we need to account for this in our calculation
|
||||||
|
)
|
||||||
|
|
||||||
|
// if (value > valueRange.start) {
|
||||||
|
SliderLabel(
|
||||||
|
label = sliderLabel, minWidth = labelMinWidth, modifier = Modifier
|
||||||
|
.padding(start = offset)
|
||||||
|
)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChanged,
|
||||||
|
valueRange = valueRange,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
|
||||||
|
steps = step,
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = MaterialTheme.colorScheme.primary,
|
||||||
|
activeTrackColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SliderLabel(label: String, minWidth: Dp, modifier: Modifier = Modifier) {
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
modifier = modifier
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
shape = RoundedCornerShape(10.dp)
|
||||||
|
)
|
||||||
|
.padding(4.dp)
|
||||||
|
.defaultMinSize(minWidth = minWidth)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getSliderOffset(
|
||||||
|
value: Float,
|
||||||
|
valueRange: ClosedFloatingPointRange<Float>,
|
||||||
|
boxWidth: Dp,
|
||||||
|
labelWidth: Dp
|
||||||
|
): Dp {
|
||||||
|
|
||||||
|
val coerced = value.coerceIn(valueRange.start, valueRange.endInclusive)
|
||||||
|
val positionFraction = calcFraction(valueRange.start, valueRange.endInclusive, coerced)
|
||||||
|
|
||||||
|
return (boxWidth - labelWidth) * positionFraction
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Calculate the 0..1 fraction that `pos` value represents between `a` and `b`
|
||||||
|
private fun calcFraction(a: Float, b: Float, pos: Float) =
|
||||||
|
(if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f)
|
||||||
@@ -323,29 +323,37 @@ fun ChipPreview() {
|
|||||||
Chip("Test Chip")
|
Chip("Test Chip")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param progress The progress of the ring as a value between 0 and 1
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun RatingRing(
|
fun RatingRing(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
progress: Float = 0f,
|
progress: Float = 0f,
|
||||||
textColor: Color = Color.White
|
size: Dp = 60.dp,
|
||||||
|
ringStrokeWidth: Dp = 4.dp,
|
||||||
|
ringColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
textColor: Color = Color.White,
|
||||||
|
textSize: TextUnit = 14.sp
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.size(60.dp)
|
.size(size)
|
||||||
.padding(8.dp)
|
// .size(60.dp)
|
||||||
|
// .padding(8.dp)
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
progress = progress,
|
progress = progress,
|
||||||
strokeWidth = 4.dp,
|
strokeWidth = ringStrokeWidth,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = ringColor
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
text = "${(progress*100).toInt()}%",
|
text = "${(progress*100).toInt()}%",
|
||||||
color = textColor,
|
color = textColor,
|
||||||
style = MaterialTheme.typography.titleSmall
|
fontSize = textSize
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ sealed class BottomNavItem(stringRes: Int, val icon: Int, val route: String): Ko
|
|||||||
val name = resourceUtils.getString(stringRes)
|
val name = resourceUtils.getString(stringRes)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val Items = listOf(Movies, TV, Favourites, Settings)
|
val Items = listOf(Movies, TV, Account, Settings)
|
||||||
|
|
||||||
fun getByRoute(route: String?): BottomNavItem? {
|
fun getByRoute(route: String?): BottomNavItem? {
|
||||||
return when (route) {
|
return when (route) {
|
||||||
Movies.route -> Movies
|
Movies.route -> Movies
|
||||||
TV.route -> TV
|
TV.route -> TV
|
||||||
|
Account.route -> Account
|
||||||
Favourites.route -> Favourites
|
Favourites.route -> Favourites
|
||||||
Settings.route -> Settings
|
Settings.route -> Settings
|
||||||
else -> null
|
else -> null
|
||||||
@@ -27,6 +28,7 @@ sealed class BottomNavItem(stringRes: Int, val icon: Int, val route: String): Ko
|
|||||||
|
|
||||||
object Movies: BottomNavItem(R.string.nav_movies_title, R.drawable.ic_movie, "movies_route")
|
object Movies: BottomNavItem(R.string.nav_movies_title, R.drawable.ic_movie, "movies_route")
|
||||||
object TV: BottomNavItem(R.string.nav_tv_title, R.drawable.ic_tv, "tv_route")
|
object TV: BottomNavItem(R.string.nav_tv_title, R.drawable.ic_tv, "tv_route")
|
||||||
|
object Account: BottomNavItem(R.string.nav_account_title, R.drawable.ic_person, "account_route")
|
||||||
object Favourites: BottomNavItem(R.string.nav_favourites_title, R.drawable.ic_favorite, "favourites_route")
|
object Favourites: BottomNavItem(R.string.nav_favourites_title, R.drawable.ic_favorite, "favourites_route")
|
||||||
object Settings: BottomNavItem(R.string.nav_settings_title, R.drawable.ic_settings, "settings_route")
|
object Settings: BottomNavItem(R.string.nav_settings_title, R.drawable.ic_settings, "settings_route")
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.owenlejeune.tvtime.ui.screens.DetailView
|
|||||||
import com.owenlejeune.tvtime.ui.screens.MainAppView
|
import com.owenlejeune.tvtime.ui.screens.MainAppView
|
||||||
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
||||||
import com.owenlejeune.tvtime.ui.screens.PersonDetailView
|
import com.owenlejeune.tvtime.ui.screens.PersonDetailView
|
||||||
|
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.AccountTab
|
||||||
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.FavouritesTab
|
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.FavouritesTab
|
||||||
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTab
|
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTab
|
||||||
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.SettingsTab
|
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.SettingsTab
|
||||||
@@ -66,6 +67,9 @@ fun BottomNavigationRoutes(
|
|||||||
composable(BottomNavItem.TV.route) {
|
composable(BottomNavItem.TV.route) {
|
||||||
MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV)
|
MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV)
|
||||||
}
|
}
|
||||||
|
composable(BottomNavItem.Account.route) {
|
||||||
|
AccountTab()
|
||||||
|
}
|
||||||
composable(BottomNavItem.Favourites.route) {
|
composable(BottomNavItem.Favourites.route) {
|
||||||
FavouritesTab()
|
FavouritesTab()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.owenlejeune.tvtime.ui.screens
|
package com.owenlejeune.tvtime.ui.screens
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
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.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
@@ -11,9 +14,11 @@ 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.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
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.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
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
|
||||||
@@ -31,11 +36,13 @@ 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.utils.SessionManager
|
||||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DetailView(
|
fun DetailView(
|
||||||
@@ -65,7 +72,7 @@ fun DetailView(
|
|||||||
.verticalScroll(state = scrollState)
|
.verticalScroll(state = scrollState)
|
||||||
) {
|
) {
|
||||||
val (
|
val (
|
||||||
backButton, backdropImage, posterImage, titleText, contentColumn
|
backButton, backdropImage, posterImage, titleText, contentColumn, ratingsView
|
||||||
) = createRefs()
|
) = createRefs()
|
||||||
|
|
||||||
Backdrop(
|
Backdrop(
|
||||||
@@ -96,6 +103,27 @@ fun DetailView(
|
|||||||
title = mediaItem.value?.title ?: "",
|
title = mediaItem.value?.title ?: "",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.size(60.dp)
|
||||||
|
.background(color = MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
.constrainAs(ratingsView) {
|
||||||
|
bottom.linkTo(titleText.top)
|
||||||
|
start.linkTo(posterImage.end, margin = 20.dp)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
RatingRing(
|
||||||
|
modifier = Modifier.padding(5.dp),
|
||||||
|
textColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
progress = mediaItem.value?.voteAverage?.let { it / 10 } ?: 0f,
|
||||||
|
textSize = 14.sp,
|
||||||
|
ringColor = MaterialTheme.colorScheme.primary,
|
||||||
|
ringStrokeWidth = 4.dp,
|
||||||
|
size = 50.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
BackButton(
|
BackButton(
|
||||||
modifier = Modifier.constrainAs(backButton) {
|
modifier = Modifier.constrainAs(backButton) {
|
||||||
top.linkTo(parent.top)//, 8.dp)
|
top.linkTo(parent.top)//, 8.dp)
|
||||||
@@ -372,6 +400,8 @@ private fun ContentColumn(
|
|||||||
MiscTvDetails(mediaItem = mediaItem, service as TvService)
|
MiscTvDetails(mediaItem = mediaItem, service as TvService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ActionsView(itemId = itemId, type = mediaType)
|
||||||
|
|
||||||
if (mediaItem.value?.overview?.isNotEmpty() == true) {
|
if (mediaItem.value?.overview?.isNotEmpty() == true) {
|
||||||
OverviewCard(mediaItem = mediaItem)
|
OverviewCard(mediaItem = mediaItem)
|
||||||
}
|
}
|
||||||
@@ -466,6 +496,111 @@ private fun MiscDetails(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActionsView(
|
||||||
|
itemId: Int?,
|
||||||
|
type: MediaViewType,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
itemId?.let {
|
||||||
|
val session = SessionManager.currentSession
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
val itemIsRated = if (type == MediaViewType.MOVIE) {
|
||||||
|
session.hasRatedMovie(itemId)
|
||||||
|
} else {
|
||||||
|
session.hasRatedTvShow(itemId)
|
||||||
|
}
|
||||||
|
val showRatingDialog = remember { mutableStateOf(false) }
|
||||||
|
ActionButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = if (itemIsRated) stringResource(R.string.delete_rating_action_label) else stringResource(R.string.rate_action_label),
|
||||||
|
onClick = {
|
||||||
|
showRatingDialog.value = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
RatingDialog(showDialog = showRatingDialog, onValueConfirmed = { rating ->
|
||||||
|
// todo post rating
|
||||||
|
Toast.makeText(context, "Rating :${rating}", Toast.LENGTH_SHORT).show()
|
||||||
|
})
|
||||||
|
if (!session.isGuest) {
|
||||||
|
ActionButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = stringResource(R.string.add_to_list_action_label),
|
||||||
|
onClick = { /*TODO*/ }
|
||||||
|
)
|
||||||
|
ActionButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = stringResource(R.string.favourite_label),
|
||||||
|
onClick = { /*TODO*/ }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RatingDialog(showDialog: MutableState<Boolean>, onValueConfirmed: (Float) -> Unit) {
|
||||||
|
|
||||||
|
fun formatPosition(position: Float): String {
|
||||||
|
return DecimalFormat("#.#").format(position/10f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDialog.value) {
|
||||||
|
var sliderPosition by remember { mutableStateOf(0f) }
|
||||||
|
AlertDialog(
|
||||||
|
modifier = Modifier.wrapContentHeight(),
|
||||||
|
onDismissRequest = { showDialog.value = false },
|
||||||
|
title = { Text(text = stringResource(R.string.rating_dialog_title)) },
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.height(40.dp),
|
||||||
|
onClick = {
|
||||||
|
onValueConfirmed.invoke(formatPosition(sliderPosition).toFloat())
|
||||||
|
showDialog.value = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.rating_dialog_confirm))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.height(40.dp),
|
||||||
|
onClick = {
|
||||||
|
showDialog.value = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
SliderWithLabel(
|
||||||
|
value = sliderPosition,
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
onValueChanged = { sliderPosition = it },
|
||||||
|
sliderLabel = formatPosition(sliderPosition)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun OverviewCard(mediaItem: MutableState<DetailedItem?>, modifier: Modifier = Modifier) {
|
private fun OverviewCard(mediaItem: MutableState<DetailedItem?>, modifier: Modifier = Modifier) {
|
||||||
ContentCard(
|
ContentCard(
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AccountTab() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.wrapContentSize(Alignment.Center)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Account",
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.owenlejeune.tvtime.utils
|
||||||
|
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.RatedMedia
|
||||||
|
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
object SessionManager: KoinComponent {
|
||||||
|
|
||||||
|
private val preferences: AppPreferences by inject()
|
||||||
|
|
||||||
|
private var _currentSession: Session? = null
|
||||||
|
val currentSession: Session
|
||||||
|
get() = _currentSession!!
|
||||||
|
|
||||||
|
private val authenticationService by lazy { TmdbClient().createAuthenticationService() }
|
||||||
|
|
||||||
|
suspend fun initialize() {
|
||||||
|
_currentSession = if (preferences.guestSessionId.isNotEmpty()) {
|
||||||
|
val session = GuestSession()
|
||||||
|
session.initialize()
|
||||||
|
session
|
||||||
|
} else {
|
||||||
|
requestNewGuestSession()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun requestNewGuestSession(): Session? {
|
||||||
|
val response = authenticationService.getNewGuestSession()
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
preferences.guestSessionId = response.body()?.guestSessionId ?: ""
|
||||||
|
_currentSession = GuestSession()
|
||||||
|
}
|
||||||
|
return _currentSession
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Session(val sessionId: String, val isGuest: Boolean) {
|
||||||
|
protected abstract var _ratedMovies: List<RatedMedia>
|
||||||
|
val ratedMovies: List<RatedMedia>
|
||||||
|
get() = _ratedMovies
|
||||||
|
|
||||||
|
protected abstract var _ratedTvShows: List<RatedMedia>
|
||||||
|
val ratedTvShows: List<RatedMedia>
|
||||||
|
get() = _ratedTvShows
|
||||||
|
|
||||||
|
protected abstract var _ratedTvEpisodes: List<RatedMedia>
|
||||||
|
val ratedTvEpisodes: List<RatedMedia>
|
||||||
|
get() = _ratedTvEpisodes
|
||||||
|
|
||||||
|
fun hasRatedMovie(id: Int): Boolean {
|
||||||
|
return ratedMovies.map { it.id }.contains(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasRatedTvShow(id: Int): Boolean {
|
||||||
|
return ratedTvShows.map { it.id }.contains(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasRatedTvEpisode(id: Int): Boolean {
|
||||||
|
return ratedTvEpisodes.map { it.id }.contains(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract suspend fun initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GuestSession: Session(preferences.guestSessionId, true) {
|
||||||
|
override var _ratedMovies: List<RatedMedia> = emptyList()
|
||||||
|
override var _ratedTvEpisodes: List<RatedMedia> = emptyList()
|
||||||
|
override var _ratedTvShows: List<RatedMedia> = emptyList()
|
||||||
|
|
||||||
|
override suspend fun initialize() {
|
||||||
|
val service = TmdbClient().createGuestSessionService()
|
||||||
|
service.getRatedMovies(sessionId).apply {
|
||||||
|
if (isSuccessful) {
|
||||||
|
_ratedMovies = body()?.results ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service.getRatedTvShows(sessionId).apply {
|
||||||
|
if (isSuccessful) {
|
||||||
|
_ratedTvShows = body()?.results ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service.getRatedTvEpisodes(sessionId).apply {
|
||||||
|
if (isSuccessful) {
|
||||||
|
_ratedTvEpisodes = body()?.results ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,6 +29,11 @@
|
|||||||
<string name="updated_at_label">Updated at: %1$s</string>
|
<string name="updated_at_label">Updated at: %1$s</string>
|
||||||
<string name="no_reviews_label">No reviews</string>
|
<string name="no_reviews_label">No reviews</string>
|
||||||
|
|
||||||
|
<string name="rate_action_label">Rate</string>
|
||||||
|
<string name="delete_rating_action_label">Delete Rating</string>
|
||||||
|
<string name="add_to_list_action_label">Add to List</string>
|
||||||
|
<string name="favourite_label">Favourite</string>
|
||||||
|
|
||||||
<!-- preferences -->
|
<!-- preferences -->
|
||||||
<string name="preference_heading_search">Search</string>
|
<string name="preference_heading_search">Search</string>
|
||||||
<string name="preferences_persistent_search_title">Persistent search bar</string>
|
<string name="preferences_persistent_search_title">Persistent search bar</string>
|
||||||
@@ -45,4 +50,8 @@
|
|||||||
<string name="video_type_featureette">Featurettes</string>
|
<string name="video_type_featureette">Featurettes</string>
|
||||||
<string name="content_description_back_button">Back</string>
|
<string name="content_description_back_button">Back</string>
|
||||||
<string name="search_icon_content_descriptor">Search Icon</string>
|
<string name="search_icon_content_descriptor">Search Icon</string>
|
||||||
|
<string name="rating_dialog_title">Add a Rating</string>
|
||||||
|
<string name="rating_dialog_confirm">Submit rating</string>
|
||||||
|
<string name="action_cancel">Cancel</string>
|
||||||
|
<string name="nav_account_title">Account</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user