sign in and display account content

This commit is contained in:
Owen LeJeune
2022-03-11 22:34:08 -05:00
parent 695af865ec
commit de4f137df9
22 changed files with 226 additions and 126 deletions

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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?
)

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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 ->

View File

@@ -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 }
}

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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
}
}
}
}
}
}

View File

@@ -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}"
}
}

View File

@@ -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>

View File

@@ -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"
}