mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-21 11:10:56 -05:00
sign in and display account content
This commit is contained in:
@@ -19,7 +19,7 @@ interface AuthenticationApi {
|
||||
suspend fun createRequestToken(): Response<CreateTokenResponse>
|
||||
|
||||
@POST("authentication/session/new")
|
||||
suspend fun createSession(@Body body: SessionBody): Response<CreateSessionResponse>
|
||||
suspend fun createSession(@Body body: TokenSessionBody): Response<CreateSessionResponse>
|
||||
|
||||
@POST("authentication/token/validate_with_login")
|
||||
suspend fun validateTokenWithLogin(@Body body: TokenValidationBody): Response<CreateTokenResponse>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.owenlejeune.tvtime.api.tmdb
|
||||
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.SessionBody
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionResponse
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.GuestSessionResponse
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||
import retrofit2.Response
|
||||
|
||||
class AuthenticationService {
|
||||
@@ -17,4 +15,15 @@ class AuthenticationService {
|
||||
return service.deleteSession(body)
|
||||
}
|
||||
|
||||
suspend fun createRequestToken(): Response<CreateTokenResponse> {
|
||||
return service.createRequestToken()
|
||||
}
|
||||
|
||||
suspend fun createSession(body: TokenSessionBody): Response<CreateSessionResponse> {
|
||||
return service.createSession(body)
|
||||
}
|
||||
|
||||
suspend fun validateTokenWithLogin(body: TokenValidationBody): Response<CreateTokenResponse> {
|
||||
return service.validateTokenWithLogin(body)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.owenlejeune.tvtime.BuildConfig
|
||||
import com.owenlejeune.tvtime.api.Client
|
||||
import com.owenlejeune.tvtime.api.QueryParam
|
||||
import com.owenlejeune.tvtime.extensions.addQueryParams
|
||||
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||
import com.owenlejeune.tvtime.utils.SessionManager
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
@@ -19,6 +20,7 @@ class TmdbClient: KoinComponent {
|
||||
}
|
||||
|
||||
private val client: Client by inject { parametersOf(BASE_URL) }
|
||||
private val preferences: AppPreferences by inject()
|
||||
|
||||
init {
|
||||
client.addInterceptor(TmdbInterceptor())
|
||||
@@ -56,11 +58,8 @@ class TmdbClient: KoinComponent {
|
||||
val languageCode = "${locale.language}-${locale.region}"
|
||||
val languageParam = QueryParam("language", languageCode)
|
||||
|
||||
var sessionIdParam: QueryParam? = null
|
||||
val segments = chain.request().url().encodedPathSegments()
|
||||
if (segments.size > 1 && segments[1].equals("account") && SessionManager.currentSession?.isAuthorized == true) {
|
||||
sessionIdParam = QueryParam("session_id", SessionManager.currentSession!!.sessionId)
|
||||
}
|
||||
val segments = chain.request().url.encodedPathSegments
|
||||
val sessionIdParam: QueryParam? = sessionIdParam(segments)
|
||||
|
||||
val request = chain.addQueryParams(apiParam, languageParam, sessionIdParam)
|
||||
|
||||
@@ -68,4 +67,16 @@ class TmdbClient: KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private fun sessionIdParam(urlSegments: List<String>): QueryParam? {
|
||||
var sessionIdParam: QueryParam? = null
|
||||
if (urlSegments.size > 1 && urlSegments[1] == "account") {
|
||||
if (SessionManager.currentSession?.isAuthorized == true) {
|
||||
sessionIdParam = QueryParam("session_id", SessionManager.currentSession!!.sessionId)
|
||||
} else if (preferences.authorizedSessionId.isNotEmpty()) {
|
||||
sessionIdParam = QueryParam("session_id", preferences.authorizedSessionId)
|
||||
}
|
||||
}
|
||||
return sessionIdParam
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,9 +13,19 @@ class AccountDetails(
|
||||
)
|
||||
|
||||
class Avatar(
|
||||
@SerializedName("gravatar") val gravatar: Gravatar
|
||||
@SerializedName("gravatar") val gravatar: Gravatar?,
|
||||
@SerializedName("tmdb") val tmdb: TmdbAvatar?
|
||||
)
|
||||
|
||||
class Gravatar(
|
||||
@SerializedName("hash") val hash: String
|
||||
@SerializedName("hash") val hash: String?
|
||||
) {
|
||||
companion object {
|
||||
private const val DEF_HASH = "88c8a9052642ec51d85d4f7beb178a97"
|
||||
}
|
||||
fun isDefault() = hash?.equals(DEF_HASH)
|
||||
}
|
||||
|
||||
class TmdbAvatar(
|
||||
@SerializedName("avatar_path") val avatarPath: String?
|
||||
)
|
||||
|
||||
@@ -4,5 +4,5 @@ import com.google.gson.annotations.SerializedName
|
||||
|
||||
class CreateSessionResponse(
|
||||
@SerializedName("success") val isSuccess: Boolean,
|
||||
@SerializedName("session_idd") val sessionId: String
|
||||
@SerializedName("session_id") val sessionId: String
|
||||
)
|
||||
|
||||
@@ -10,10 +10,10 @@ class RatedEpisode(
|
||||
voteAverage: Float,
|
||||
voteCount: Int,
|
||||
rating: Float,
|
||||
@SerializedName("air_date") val airDate: String,
|
||||
releaseDate: String,
|
||||
@SerializedName("episode_number") val episodeNumber: Int,
|
||||
@SerializedName("production_code") val productionCode: String?,
|
||||
@SerializedName("season_number") val seasonNumber: Int,
|
||||
@SerializedName("show_id") val showId: Int,
|
||||
@SerializedName("still_path") val stillPath: String?,
|
||||
): RatedMedia(RatedType.EPISODE, id, overview, name, voteAverage, voteCount, rating)
|
||||
): RatedMedia(RatedType.EPISODE, id, overview, name, voteAverage, voteCount, rating, releaseDate)
|
||||
@@ -9,7 +9,8 @@ abstract class RatedMedia(
|
||||
@SerializedName("name", alternate = ["title"]) val name: String,
|
||||
@SerializedName("vote_average") val voteAverage: Float,
|
||||
@SerializedName("vote_count") val voteCount: Int,
|
||||
@SerializedName("rating") val rating: Float
|
||||
@SerializedName("rating") val rating: Float,
|
||||
@SerializedName("release_date", alternate = ["first_air_date", "air_date"]) val releaseDate: String
|
||||
) {
|
||||
enum class RatedType {
|
||||
MOVIE,
|
||||
|
||||
@@ -15,10 +15,10 @@ class RatedMovie(
|
||||
originalName: String,
|
||||
posterPath: String?,
|
||||
popularity: Float,
|
||||
releaseDate: String,
|
||||
@SerializedName("adult") val isAdult: Boolean,
|
||||
@SerializedName("release_date") val releaseDate: String,
|
||||
@SerializedName("video") val video: Boolean,
|
||||
): RatedTopLevelMedia(
|
||||
RatedType.MOVIE, id, overview, name, voteAverage, voteCount, rating,
|
||||
RatedType.MOVIE, id, overview, name, voteAverage, voteCount, rating, releaseDate,
|
||||
backdropPath, genreIds, originalLanguage, originalName, posterPath, popularity
|
||||
)
|
||||
@@ -10,10 +10,11 @@ abstract class RatedTopLevelMedia(
|
||||
voteAverage: Float,
|
||||
voteCount: Int,
|
||||
rating: Float,
|
||||
releaseDate: String,
|
||||
@SerializedName("backdrop_path") val backdropPath: String?,
|
||||
@SerializedName("genre_ids") val genreIds: List<Int>,
|
||||
@SerializedName("original_language") val originalLanguage: String,
|
||||
@SerializedName("original_name", alternate = ["original_title"]) val originalName: String,
|
||||
@SerializedName("poster_path") val posterPath: String?,
|
||||
@SerializedName("popularity") val popularity: Float,
|
||||
): RatedMedia(type, id, overview, name, voteAverage, voteCount, rating)
|
||||
): RatedMedia(type, id, overview, name, voteAverage, voteCount, rating, releaseDate)
|
||||
@@ -15,9 +15,9 @@ class RatedTv(
|
||||
originalName: String,
|
||||
posterPath: String?,
|
||||
popularity: Float,
|
||||
@SerializedName("first_air_date") val firstAirDate: String,
|
||||
releaseDate: String,
|
||||
@SerializedName("origin_country") val originCountry: List<String>,
|
||||
): RatedTopLevelMedia(
|
||||
RatedType.SERIES, id, overview, name, voteAverage, voteCount, rating,
|
||||
RatedType.SERIES, id, overview, name, voteAverage, voteCount, rating, releaseDate,
|
||||
backdropPath, genreIds, originalLanguage, originalName, posterPath, popularity
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.owenlejeune.tvtime.api.tmdb.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class TokenSessionBody(
|
||||
@SerializedName("request_token") val requestToken: String
|
||||
)
|
||||
@@ -6,7 +6,7 @@ import okhttp3.Request
|
||||
|
||||
fun Interceptor.Chain.addQueryParams(vararg queryParams: QueryParam?): Request {
|
||||
val original = request()
|
||||
val originalHttpUrl = original.url()
|
||||
val originalHttpUrl = original.url
|
||||
|
||||
val urlBuilder = originalHttpUrl.newBuilder()
|
||||
queryParams.forEach { param ->
|
||||
|
||||
@@ -5,4 +5,8 @@ import android.util.Patterns
|
||||
|
||||
fun String.isEmailValid(): Boolean {
|
||||
return !TextUtils.isEmpty(this) && Patterns.EMAIL_ADDRESS.matcher(this).matches()
|
||||
}
|
||||
|
||||
fun String.unlessEmpty(other: String): String {
|
||||
return this.ifEmpty { other }
|
||||
}
|
||||
@@ -58,7 +58,7 @@ fun CustomTopAppBarDropdownMenu(
|
||||
) {
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
|
||||
Box(modifier = Modifier.wrapContentSize(Alignment.TopEnd)) {
|
||||
Box(modifier = Modifier.wrapContentSize(Alignment.TopEnd).padding(end = 12.dp)) {
|
||||
IconButton(onClick = { expanded.value = true }) {
|
||||
icon()
|
||||
}
|
||||
|
||||
@@ -9,17 +9,31 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.extensions.isEmailValid
|
||||
import com.owenlejeune.tvtime.utils.SessionManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun SignInDialog(
|
||||
showDialog: MutableState<Boolean>,
|
||||
@@ -27,33 +41,34 @@ fun SignInDialog(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var emailState by rememberSaveable { mutableStateOf("") }
|
||||
var emailHasErrors by rememberSaveable { mutableStateOf(false) }
|
||||
var emailError = ""
|
||||
var usernameState by rememberSaveable { mutableStateOf("") }
|
||||
var usernameHasErrors by rememberSaveable { mutableStateOf(false) }
|
||||
var usernameError = ""
|
||||
|
||||
var passwordState by rememberSaveable { mutableStateOf("") }
|
||||
var passwordHasErrors by rememberSaveable { mutableStateOf(false) }
|
||||
var passwordError = ""
|
||||
|
||||
fun validate(): Boolean {
|
||||
emailError = ""
|
||||
usernameError = ""
|
||||
passwordError = ""
|
||||
|
||||
if (TextUtils.isEmpty(emailState)) {
|
||||
emailError = context.getString(R.string.email_not_empty_error)
|
||||
} else if (!emailState.isEmailValid()) {
|
||||
emailError = context.getString(R.string.email_invalid_error)
|
||||
if (TextUtils.isEmpty(usernameState)) {
|
||||
usernameError = context.getString(R.string.username_not_empty_error)
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(passwordState)) {
|
||||
passwordError = context.getString(R.string.password_empty_error)
|
||||
}
|
||||
|
||||
emailHasErrors = emailError.isNotEmpty()
|
||||
usernameHasErrors = usernameError.isNotEmpty()
|
||||
passwordHasErrors = passwordError.isNotEmpty()
|
||||
|
||||
return !emailHasErrors && !passwordHasErrors
|
||||
return !usernameHasErrors && !passwordHasErrors
|
||||
}
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.action_sign_in)) },
|
||||
onDismissRequest = { showDialog.value = false },
|
||||
@@ -66,14 +81,15 @@ fun SignInDialog(
|
||||
text = stringResource(R.string.sign_in_dialog_message)
|
||||
)
|
||||
ThemedOutlineTextField(
|
||||
value = emailState,
|
||||
value = usernameState,
|
||||
onValueChange = {
|
||||
emailHasErrors = false
|
||||
emailState = it
|
||||
usernameHasErrors = false
|
||||
usernameState = it
|
||||
},
|
||||
label = { Text(text = stringResource(R.string.email_label)) },
|
||||
isError = emailHasErrors,
|
||||
errorMessage = emailError
|
||||
label = { Text(text = stringResource(R.string.username_label)) },
|
||||
isError = usernameHasErrors,
|
||||
errorMessage = usernameError,
|
||||
singleLine = true
|
||||
)
|
||||
PasswordOutlineTextField(
|
||||
value = passwordState,
|
||||
@@ -83,9 +99,10 @@ fun SignInDialog(
|
||||
},
|
||||
label = { Text(text = stringResource(R.string.password_label)) },
|
||||
isError = passwordHasErrors,
|
||||
errorMessage = passwordError
|
||||
errorMessage = passwordError,
|
||||
singleLine = true
|
||||
)
|
||||
SignInButton(validate = ::validate) { success ->
|
||||
SignInButton(username = usernameState, password = passwordState, validate = ::validate) { success ->
|
||||
if (success) {
|
||||
showDialog.value = false
|
||||
} else {
|
||||
@@ -107,7 +124,7 @@ private fun CancelButton(showDialog: MutableState<Boolean>) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SignInButton(validate: () -> Boolean, onSuccess: (success: Boolean) -> Unit) {
|
||||
private fun SignInButton(username: String, password: String, validate: () -> Boolean, onSuccess: (success: Boolean) -> Unit) {
|
||||
var signInInProgress by remember { mutableStateOf(false) }
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -115,13 +132,13 @@ private fun SignInButton(validate: () -> Boolean, onSuccess: (success: Boolean)
|
||||
if (!signInInProgress) {
|
||||
if (validate()) {
|
||||
signInInProgress = true
|
||||
// signIn(context, emailState, passwordState) { success ->
|
||||
// signInInProgress = false
|
||||
// if (success) {
|
||||
// showDialog.value = false
|
||||
// }
|
||||
// }
|
||||
onSuccess(false)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val success = SessionManager.signInWithLogin(username, password)
|
||||
withContext(Dispatchers.Main) {
|
||||
signInInProgress = false
|
||||
onSuccess(success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +150,7 @@ private fun SignInButton(validate: () -> Boolean, onSuccess: (success: Boolean)
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(text = stringResource(id = R.string.action_sign_in))
|
||||
Text(text = stringResource(id = R.string.action_sign_in), color = MaterialTheme.colorScheme.background)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -794,6 +794,7 @@ fun PasswordOutlineTextField(
|
||||
isError: Boolean = false,
|
||||
errorMessage: String = "",
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
@@ -818,7 +819,7 @@ fun PasswordOutlineTextField(
|
||||
interactionSource = interactionSource,
|
||||
shape = shape,
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
keyboardOptions = keyboardOptions.copy(keyboardType = KeyboardType.Password),
|
||||
trailingIcon = {
|
||||
val image = if (passwordVisible) {
|
||||
Icons.Filled.Visibility
|
||||
|
||||
@@ -622,7 +622,7 @@ private fun ReviewsCard(
|
||||
fetchReviews(itemId, service, reviewsResponse)
|
||||
}
|
||||
}
|
||||
// > 0
|
||||
|
||||
val hasReviews = reviewsResponse.value?.results?.size?.let { it > 0 }
|
||||
val m = if (hasReviews == true) {
|
||||
modifier.height(400.dp)
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -14,12 +15,18 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavHostController
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.rememberImagePainter
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.ImageResult
|
||||
import coil.transform.CircleCropTransformation
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.PagerState
|
||||
@@ -42,7 +49,6 @@ import kotlin.reflect.KClass
|
||||
|
||||
private const val GUEST_SIGN_IN = "guest_sign_in"
|
||||
private const val SIGN_OUT = "sign_out"
|
||||
private const val ACCOUNT_SIGN_OUT = "account_sign_out"
|
||||
private const val NO_SESSION_SIGN_IN = "no_session_sign_in"
|
||||
private const val NO_SESSION_SIGN_IN_GUEST = "no_session_sign_in_guest"
|
||||
|
||||
@@ -53,10 +59,18 @@ fun AccountTab(
|
||||
appBarTitle: MutableState<String>,
|
||||
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({})
|
||||
) {
|
||||
if (SessionManager.currentSession?.isAuthorized == false) {
|
||||
appBarTitle.value = stringResource(id = R.string.account_header_title_formatted).replace("%1\$s", stringResource(id = R.string.account_name_guest))
|
||||
} else {
|
||||
appBarTitle.value = stringResource(id = R.string.account_not_logged_in)
|
||||
when (SessionManager.currentSession?.isAuthorized) {
|
||||
false -> {
|
||||
appBarTitle.value =
|
||||
stringResource(id = R.string.account_header_title_formatted, stringResource(id = R.string.account_name_guest))
|
||||
}
|
||||
true -> {
|
||||
appBarTitle.value =
|
||||
stringResource(id = R.string.account_header_title_formatted, getAccountName(SessionManager.currentSession?.accountDetails))
|
||||
}
|
||||
else -> {
|
||||
appBarTitle.value = stringResource(id = R.string.account_not_logged_in)
|
||||
}
|
||||
}
|
||||
|
||||
val lastSelectedOption = remember { mutableStateOf("") }
|
||||
@@ -86,6 +100,17 @@ fun AccountTab(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAccountName(accountDetails: AccountDetails?): String {
|
||||
if (accountDetails != null) {
|
||||
if (accountDetails.name.isNotEmpty()) {
|
||||
return accountDetails.name
|
||||
} else if (accountDetails.username.isNotEmpty()) {
|
||||
return accountDetails.username
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T: Any> AccountTabContent(
|
||||
appNavController: NavHostController,
|
||||
@@ -113,8 +138,8 @@ fun <T: Any> AccountTabContent(
|
||||
} else {
|
||||
items(contentItems.size) { i ->
|
||||
when (clazz) {
|
||||
RatedMovie::class -> {
|
||||
val item = contentItems[i] as RatedMovie
|
||||
RatedTv::class, RatedMovie::class -> {
|
||||
val item = contentItems[i] as RatedTopLevelMedia
|
||||
MediaItemRow(
|
||||
appNavController = appNavController,
|
||||
mediaViewType = mediaViewType,
|
||||
@@ -125,32 +150,19 @@ fun <T: Any> AccountTabContent(
|
||||
rating = item.rating
|
||||
)
|
||||
}
|
||||
RatedTv::class -> {
|
||||
val item = contentItems[i] as RatedTv
|
||||
MediaItemRow(
|
||||
appNavController = appNavController,
|
||||
mediaViewType = mediaViewType,
|
||||
id = item.id,
|
||||
posterPath = TmdbUtils.getFullPosterPath(item.posterPath),
|
||||
name = item.name,
|
||||
date = item.firstAirDate,
|
||||
rating = item.rating
|
||||
)
|
||||
}
|
||||
RatedEpisode::class -> {
|
||||
val item = contentItems[i] as RatedEpisode
|
||||
val item = contentItems[i] as RatedMedia
|
||||
MediaItemRow(
|
||||
appNavController = appNavController,
|
||||
mediaViewType = mediaViewType,
|
||||
id = item.id,
|
||||
posterPath = null,
|
||||
name = item.name,
|
||||
date = item.airDate,
|
||||
date = item.releaseDate,
|
||||
rating = item.rating
|
||||
)
|
||||
}
|
||||
FavoriteMovie::class -> {
|
||||
val item = contentItems[i] as FavoriteMovie
|
||||
FavoriteMovie::class, FavoriteTvSeries::class -> {
|
||||
val item = contentItems[i] as FavoriteMedia
|
||||
MediaItemRow(
|
||||
appNavController = appNavController,
|
||||
mediaViewType = mediaViewType,
|
||||
@@ -160,30 +172,8 @@ fun <T: Any> AccountTabContent(
|
||||
date = item.releaseDate
|
||||
)
|
||||
}
|
||||
FavoriteTvSeries::class -> {
|
||||
val item = contentItems[i] as FavoriteTvSeries
|
||||
MediaItemRow(
|
||||
appNavController = appNavController,
|
||||
mediaViewType = mediaViewType,
|
||||
id = item.id,
|
||||
posterPath = TmdbUtils.getFullPosterPath(item.posterPath),
|
||||
name = item.title,
|
||||
date = item.releaseDate
|
||||
)
|
||||
}
|
||||
WatchlistMovie::class -> {
|
||||
val item = contentItems[i] as WatchlistMovie
|
||||
MediaItemRow(
|
||||
appNavController = appNavController,
|
||||
mediaViewType = mediaViewType,
|
||||
id = item.id,
|
||||
posterPath = TmdbUtils.getFullPosterPath(item.posterPath),
|
||||
name = item.title,
|
||||
date = item.releaseDate
|
||||
)
|
||||
}
|
||||
WatchlistTvSeries::class -> {
|
||||
val item = contentItems[i] as WatchlistTvSeries
|
||||
WatchlistMovie::class, WatchlistTvSeries::class -> {
|
||||
val item = contentItems[i] as WatchlistMedia
|
||||
MediaItemRow(
|
||||
appNavController = appNavController,
|
||||
mediaViewType = mediaViewType,
|
||||
@@ -204,9 +194,9 @@ private fun MediaItemRow(
|
||||
appNavController: NavHostController,
|
||||
mediaViewType: MediaViewType,
|
||||
id: Int,
|
||||
posterPath: String?,
|
||||
name: String,
|
||||
date: String,
|
||||
posterPath: String? = null,
|
||||
rating: Float? = null
|
||||
) {
|
||||
Row(
|
||||
@@ -262,7 +252,7 @@ private fun AccountDropdownMenu(
|
||||
CustomTopAppBarDropdownMenu(
|
||||
icon = {
|
||||
when(session?.isAuthorized) {
|
||||
true -> { }
|
||||
true -> { AuthorizedSessionIcon() }
|
||||
false -> { GuestSessionIcon() }
|
||||
null -> { NoSessionAccountIcon() }
|
||||
}
|
||||
@@ -312,9 +302,7 @@ private fun NoSessionMenuItems(
|
||||
@Composable
|
||||
private fun NoSessionAccountIcon() {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(50.dp)
|
||||
.padding(end = 8.dp),
|
||||
modifier = Modifier.size(45.dp),
|
||||
imageVector = Icons.Filled.AccountCircle,
|
||||
contentDescription = stringResource(R.string.account_menu_content_description),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
@@ -366,12 +354,43 @@ private fun AuthorizedSessionMenuItems(
|
||||
CustomMenuItem(
|
||||
text = stringResource(id = R.string.action_sign_out),
|
||||
onClick = {
|
||||
lastSelectedOption.value = ACCOUNT_SIGN_OUT
|
||||
signOut(lastSelectedOption)
|
||||
expanded.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AuthorizedSessionIcon() {
|
||||
val accountDetails = SessionManager.currentSession?.accountDetails
|
||||
val avatarUrl = accountDetails?.let {
|
||||
when {
|
||||
accountDetails.avatar.tmdb?.avatarPath?.isNotEmpty() == true -> {
|
||||
TmdbUtils.getAccountAvatarUrl(accountDetails)
|
||||
}
|
||||
accountDetails.avatar.gravatar?.isDefault() == false -> {
|
||||
TmdbUtils.getAccountGravatarUrl(accountDetails)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
if (accountDetails == null || avatarUrl == null) {
|
||||
val accLetter = (accountDetails?.name?.ifEmpty { accountDetails.username } ?: " ")[0]
|
||||
RoundedLetterImage(size = 40.dp, character = accLetter, topPadding = 40.dp / 8)
|
||||
} else {
|
||||
Box(modifier = Modifier.size(50.dp)) {
|
||||
AsyncImage(
|
||||
model = avatarUrl,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createGuestSession(lastSelectedOption: MutableState<String>) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val session = SessionManager.requestNewGuestSession()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.owenlejeune.tvtime.utils
|
||||
|
||||
import com.owenlejeune.tvtime.api.tmdb.AccountService
|
||||
import com.owenlejeune.tvtime.api.tmdb.AuthenticationService
|
||||
import com.owenlejeune.tvtime.api.tmdb.GuestSessionService
|
||||
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||
@@ -25,11 +26,7 @@ object SessionManager: KoinComponent {
|
||||
fun clearSession(onResponse: (isSuccessful: Boolean) -> Unit) {
|
||||
currentSession?.let { session ->
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val deleteResponse = authenticationService.deleteSession(
|
||||
SessionBody(
|
||||
session.sessionId
|
||||
)
|
||||
)
|
||||
val deleteResponse = authenticationService.deleteSession(SessionBody(session.sessionId))
|
||||
withContext(Dispatchers.Main) {
|
||||
if (deleteResponse.isSuccessful) {
|
||||
_currentSession = null
|
||||
@@ -48,7 +45,9 @@ object SessionManager: KoinComponent {
|
||||
session.initialize()
|
||||
_currentSession = session
|
||||
} else if (preferences.authorizedSessionId.isNotEmpty()) {
|
||||
|
||||
val session = AuthorizedSession()
|
||||
session.initialize()
|
||||
_currentSession = session
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,20 +60,29 @@ object SessionManager: KoinComponent {
|
||||
return _currentSession
|
||||
}
|
||||
|
||||
suspend fun signInWithLogin(email: String, password: String): Boolean {
|
||||
val service = TmdbClient().createAuthenticationService()
|
||||
suspend fun signInWithLogin(username: String, password: String): Boolean {
|
||||
val service = AuthenticationService()
|
||||
val createTokenResponse = service.createRequestToken()
|
||||
if (createTokenResponse.isSuccessful) {
|
||||
createTokenResponse.body()?.let { ctr ->
|
||||
val body = TokenValidationBody(email, password, ctr.requestToken)
|
||||
val body = TokenValidationBody(username, password, ctr.requestToken)
|
||||
val loginResponse = service.validateTokenWithLogin(body)
|
||||
if (loginResponse.isSuccessful) {
|
||||
loginResponse.body()?.let { lr ->
|
||||
if (lr.success) {
|
||||
preferences.authorizedSessionId = lr.requestToken
|
||||
_currentSession = AuthorizedSession()
|
||||
_currentSession?.initialize()
|
||||
return true
|
||||
val sessionBody = TokenSessionBody(lr.requestToken)
|
||||
val sessionResponse = service.createSession(sessionBody)
|
||||
if (sessionResponse.isSuccessful) {
|
||||
sessionResponse.body()?.let { sr ->
|
||||
if (sr.isSuccess) {
|
||||
preferences.authorizedSessionId = sr.sessionId
|
||||
preferences.guestSessionId = ""
|
||||
_currentSession = AuthorizedSession()
|
||||
_currentSession?.initialize()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ object TmdbUtils {
|
||||
private const val POSTER_BASE = "https://image.tmdb.org/t/p/original"
|
||||
private const val BACKDROP_BASE = "https://www.themoviedb.org/t/p/original"
|
||||
private const val PERSON_BASE = "https://www.themoviedb.org/t/p/w600_and_h900_bestv2"
|
||||
private const val GRAVATAR_BASE = "https://www.gravatar.com/avatar/"
|
||||
private const val AVATAR_BASE = "https://www.themoviedb.org/t/p/w150_and_h150_face"
|
||||
|
||||
private const val DEF_REGION = "US"
|
||||
|
||||
fun getFullPosterPath(posterPath: String?): String? {
|
||||
return posterPath?.let { "https://image.tmdb.org/t/p/original${posterPath}" }
|
||||
return posterPath?.let { "${POSTER_BASE}${posterPath}" }
|
||||
}
|
||||
|
||||
fun getFullPosterPath(tmdbItem: TmdbItem?): String? {
|
||||
@@ -25,7 +27,7 @@ object TmdbUtils {
|
||||
}
|
||||
|
||||
fun getFullBackdropPath(backdropPath: String?): String? {
|
||||
return backdropPath?.let { "https://www.themoviedb.org/t/p/original${backdropPath}" }
|
||||
return backdropPath?.let { "${BACKDROP_BASE}${backdropPath}" }
|
||||
}
|
||||
|
||||
fun getFullBackdropPath(detailItem: DetailedItem?): String? {
|
||||
@@ -37,7 +39,7 @@ object TmdbUtils {
|
||||
}
|
||||
|
||||
fun getFullPersonImagePath(path: String?): String? {
|
||||
return path?.let { "https://www.themoviedb.org/t/p/w600_and_h900_bestv2${path}" }
|
||||
return path?.let { "${PERSON_BASE}${path}" }
|
||||
}
|
||||
|
||||
fun getFullPersonImagePath(person: Person): String? {
|
||||
@@ -49,7 +51,7 @@ object TmdbUtils {
|
||||
if (path.contains("http")) {
|
||||
return path.substring(startIndex = 1)
|
||||
}
|
||||
"https://www.themoviedb.org/t/p/w150_and_h150_face${path}"
|
||||
"${AVATAR_BASE}${path}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,4 +164,14 @@ object TmdbUtils {
|
||||
return formatter.format(date)
|
||||
}
|
||||
|
||||
fun getAccountGravatarUrl(accountDetails: AccountDetails): String {
|
||||
val hash = accountDetails.avatar.gravatar?.hash
|
||||
return "${GRAVATAR_BASE}${hash}"
|
||||
}
|
||||
|
||||
fun getAccountAvatarUrl(accountDetails: AccountDetails): String {
|
||||
val path = accountDetails.avatar.tmdb?.avatarPath
|
||||
return "${AVATAR_BASE}${path}"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -92,11 +92,11 @@
|
||||
<string name="action_sign_in_as_guest">Sign In as Guest</string>
|
||||
<string name="account_menu_content_description">Account Menu</string>
|
||||
<string name="action_sign_out">Sign Out</string>
|
||||
<string name="email_not_empty_error">Email must not be empty</string>
|
||||
<string name="username_not_empty_error">Username must not be empty</string>
|
||||
<string name="email_invalid_error">Invalid email</string>
|
||||
<string name="password_empty_error">Must enter a password</string>
|
||||
<string name="sign_in_dialog_message">Sign in using your TMDB credentials</string>
|
||||
<string name="email_label">Email</string>
|
||||
<string name="username_label">Username</string>
|
||||
<string name="password_label">Password</string>
|
||||
<string name="no_account_message">Don\'t have an account?</string>
|
||||
</resources>
|
||||
@@ -22,6 +22,6 @@ object Versions {
|
||||
const val gson = "2.8.7"
|
||||
const val koin = "3.1.4"
|
||||
const val paging = "3.1.0"
|
||||
const val coil = "1.4.0"
|
||||
const val coil = "2.0.0-rc01"
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user