diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationApi.kt index 72069c5..0eb14a1 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationApi.kt @@ -19,7 +19,7 @@ interface AuthenticationApi { suspend fun createRequestToken(): Response @POST("authentication/session/new") - suspend fun createSession(@Body body: SessionBody): Response + suspend fun createSession(@Body body: TokenSessionBody): Response @POST("authentication/token/validate_with_login") suspend fun validateTokenWithLogin(@Body body: TokenValidationBody): Response diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationService.kt index 6331d8f..aa687e4 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/AuthenticationService.kt @@ -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 { + return service.createRequestToken() + } + + suspend fun createSession(body: TokenSessionBody): Response { + return service.createSession(body) + } + + suspend fun validateTokenWithLogin(body: TokenValidationBody): Response { + return service.validateTokenWithLogin(body) + } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt index 9ab9fb9..e61d203 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt @@ -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): 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 + } + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/AccountDetails.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/AccountDetails.kt index f667378..eb27ede 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/AccountDetails.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/AccountDetails.kt @@ -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? ) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/CreateSessionResponse.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/CreateSessionResponse.kt index 067dec1..2249a1a 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/CreateSessionResponse.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/CreateSessionResponse.kt @@ -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 ) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedEpisode.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedEpisode.kt index b4682aa..ff68282 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedEpisode.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedEpisode.kt @@ -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) \ No newline at end of file +): RatedMedia(RatedType.EPISODE, id, overview, name, voteAverage, voteCount, rating, releaseDate) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMedia.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMedia.kt index 2b0769a..4d8ad4a 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMedia.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMedia.kt @@ -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, diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMovie.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMovie.kt index 3edcb16..bda661c 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMovie.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedMovie.kt @@ -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 ) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedTopLevelMedia.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedTopLevelMedia.kt index a0c164d..c334905 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedTopLevelMedia.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedTopLevelMedia.kt @@ -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, @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) \ No newline at end of file +): RatedMedia(type, id, overview, name, voteAverage, voteCount, rating, releaseDate) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedTv.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedTv.kt index fe4cd82..991c5ab 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedTv.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/RatedTv.kt @@ -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, ): RatedTopLevelMedia( - RatedType.SERIES, id, overview, name, voteAverage, voteCount, rating, + RatedType.SERIES, id, overview, name, voteAverage, voteCount, rating, releaseDate, backdropPath, genreIds, originalLanguage, originalName, posterPath, popularity ) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/TokenSessionBody.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/TokenSessionBody.kt new file mode 100644 index 0000000..9eaf41c --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/TokenSessionBody.kt @@ -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 +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/InterceptorExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/InterceptorExtensions.kt index 1425c80..efe5afd 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/extensions/InterceptorExtensions.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/InterceptorExtensions.kt @@ -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 -> diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/StringExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/StringExtensions.kt index 21669b8..e9378f1 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/extensions/StringExtensions.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/StringExtensions.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Menus.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Menus.kt index 99cac23..a2d023b 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Menus.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Menus.kt @@ -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() } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/SignInDialog.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/SignInDialog.kt index 7e2ff63..4395507 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/SignInDialog.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/SignInDialog.kt @@ -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, @@ -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) { } @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) } } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt index 205aae5..2757b35 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt @@ -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 diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailView.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailView.kt index f75f197..e4a32ba 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailView.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailView.kt @@ -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) diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt index 92c3f53..fe9e8f8 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt @@ -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, 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 AccountTabContent( appNavController: NavHostController, @@ -113,8 +138,8 @@ fun 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 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 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) { CoroutineScope(Dispatchers.IO).launch { val session = SessionManager.requestNewGuestSession() diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt index f2e561e..2b8e04f 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt @@ -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 + } + } + } } } } diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt index 6831654..aaf62ce 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt @@ -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}" + } + } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 704ec84..f65b999 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -92,11 +92,11 @@ Sign In as Guest Account Menu Sign Out - Email must not be empty + Username must not be empty Invalid email Must enter a password Sign in using your TMDB credentials - Email + Username Password Don\'t have an account? \ No newline at end of file diff --git a/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Versions.kt b/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Versions.kt index 09d5eca..203405c 100644 --- a/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Versions.kt +++ b/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Versions.kt @@ -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" } \ No newline at end of file