diff --git a/app/build.gradle b/app/build.gradle index 7f3f4e0..3b6a9fe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -59,6 +59,7 @@ dependencies { implementation "androidx.compose.ui:ui:${Versions.compose}" implementation "androidx.compose.material3:material3:${Versions.compose_material3}" implementation "androidx.compose.material:material:${Versions.compose}" + implementation "androidx.compose.material:material-icons-extended:${Versions.compose}" implementation "androidx.compose.ui:ui-tooling-preview:${Versions.compose}" implementation "androidx.activity:activity-compose:${Versions.activity_compose}" implementation "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}" 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 9bb7202..72069c5 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 @@ -1,21 +1,26 @@ 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 com.owenlejeune.tvtime.api.tmdb.model.* import retrofit2.Response import retrofit2.http.Body -import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.HTTP +import retrofit2.http.POST interface AuthenticationApi { @GET("authentication/guest_session/new") suspend fun getNewGuestSession(): Response -// @DELETE("authentication/session") @HTTP(method = "DELETE", path = "authentication/session", hasBody = true) - suspend fun deleteSession(@Body body: DeleteSessionBody): Response + suspend fun deleteSession(@Body body: SessionBody): Response + @GET("authentication/token/new") + suspend fun createRequestToken(): Response + + @POST("authentication/session/new") + suspend fun createSession(@Body body: SessionBody): Response + + @POST("authentication/token/validate_with_login") + suspend fun validateTokenWithLogin(@Body body: TokenValidationBody): Response } \ No newline at end of file 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 fa0ade7..6331d8f 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,6 +1,6 @@ package com.owenlejeune.tvtime.api.tmdb -import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionBody +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 retrofit2.Response @@ -13,7 +13,7 @@ class AuthenticationService { return service.getNewGuestSession() } - suspend fun deleteSession(body: DeleteSessionBody): Response { + suspend fun deleteSession(body: SessionBody): Response { return service.deleteSession(body) } diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt index add647f..e236d73 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt @@ -55,7 +55,7 @@ class MoviesService: KoinComponent, DetailService, HomePageService { override suspend fun postRating(id: Int, rating: RatingBody): Response { val session = SessionManager.currentSession ?: throw Exception("Session must not be null") - return if (session.isGuest) { + return if (!session.isAuthorized) { movieService.postMovieRatingAsGuest(id, session.sessionId, rating) } else { movieService.postMovieRatingAsUser(id, session.sessionId, rating) @@ -64,7 +64,7 @@ class MoviesService: KoinComponent, DetailService, HomePageService { override suspend fun deleteRating(id: Int): Response { val session = SessionManager.currentSession ?: throw Exception("Session must not be null") - return if (session.isGuest) { + return if (!session.isAuthorized) { movieService.deleteMovieReviewAsGuest(id, session.sessionId) } else { movieService.deleteMovieReviewAsUser(id, session.sessionId) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt index 4059a97..ab60f21 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt @@ -55,7 +55,7 @@ class TvService: KoinComponent, DetailService, HomePageService { override suspend fun postRating(id: Int, rating: RatingBody): Response { val session = SessionManager.currentSession ?: throw Exception("Session must not be null") - return if (session.isGuest) { + return if (!session.isAuthorized) { service.postTvRatingAsGuest(id, session.sessionId, rating) } else { service.postTvRatingAsUser(id, session.sessionId, rating) @@ -64,7 +64,7 @@ class TvService: KoinComponent, DetailService, HomePageService { override suspend fun deleteRating(id: Int): Response { val session = SessionManager.currentSession ?: throw Exception("Session must not be null") - return if (session.isGuest) { + return if (!session.isAuthorized) { service.deleteTvReviewAsGuest(id, session.sessionId) } else { service.deleteTvReviewAsUser(id, session.sessionId) 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 new file mode 100644 index 0000000..067dec1 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/CreateSessionResponse.kt @@ -0,0 +1,8 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class CreateSessionResponse( + @SerializedName("success") val isSuccess: Boolean, + @SerializedName("session_idd") val sessionId: String +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/CreateTokenResponse.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/CreateTokenResponse.kt new file mode 100644 index 0000000..9af09c7 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/CreateTokenResponse.kt @@ -0,0 +1,9 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class CreateTokenResponse( + @SerializedName("success") val success: Boolean, + @SerializedName("request_token") val requestToken: String, + @SerializedName("expires_at") val expiry: String +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DeleteSessionBody.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/SessionBody.kt similarity index 86% rename from app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DeleteSessionBody.kt rename to app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/SessionBody.kt index 3cf7525..ea52a2f 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DeleteSessionBody.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/SessionBody.kt @@ -2,6 +2,6 @@ package com.owenlejeune.tvtime.api.tmdb.model import com.google.gson.annotations.SerializedName -class DeleteSessionBody( +class SessionBody( @SerializedName("session_id") val sessionsId: String ) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/TokenValidationBody.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/TokenValidationBody.kt new file mode 100644 index 0000000..fcda286 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/TokenValidationBody.kt @@ -0,0 +1,9 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class TokenValidationBody( + @SerializedName("username") val username: String, + @SerializedName("password") val password: String, + @SerializedName("request_token") val requestToken: String +) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/StringExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/StringExtensions.kt new file mode 100644 index 0000000..21669b8 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/StringExtensions.kt @@ -0,0 +1,8 @@ +package com.owenlejeune.tvtime.extensions + +import android.text.TextUtils +import android.util.Patterns + +fun String.isEmailValid(): Boolean { + return !TextUtils.isEmpty(this) && Patterns.EMAIL_ADDRESS.matcher(this).matches() +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt index d9953a0..49b1cac 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt @@ -12,6 +12,7 @@ class AppPreferences(context: Context) { private val PERSISTENT_SEARCH = "persistent_search" private val HIDE_TITLE = "hide_title" private val GUEST_SESSION = "guest_session_id" + private val AUTHORIZED_SESSION = "authorized_session_id" } private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE) @@ -27,6 +28,10 @@ class AppPreferences(context: Context) { var guestSessionId: String get() = preferences.getString(GUEST_SESSION, "") ?: "" set(value) { preferences.put(GUEST_SESSION, value) } + + var authorizedSessionId: String + get() = preferences.getString(AUTHORIZED_SESSION, "") ?: "" + set(value) { preferences.put(AUTHORIZED_SESSION, value) } // val usePreferences: MutableState // var usePreferences: Boolean // get() = preferences.getBoolean(USE_PREFERENCES, false) 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 new file mode 100644 index 0000000..2c5a5cb --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Menus.kt @@ -0,0 +1,81 @@ +package com.owenlejeune.tvtime.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.AlertDialog +import androidx.compose.material.DropdownMenu +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Dialog + +@Composable +fun TopAppBarDropdownMenu( + icon: @Composable () -> Unit = {}, + content: @Composable ColumnScope.(expanded: MutableState) -> Unit = {} +) { + val expanded = remember { mutableStateOf(false) } + + Box( + modifier = Modifier.wrapContentSize(Alignment.TopEnd) + ) { + IconButton( + onClick = { + expanded.value = true + } + ) { + icon() + } + } + + DropdownMenu( + modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceVariant), + expanded = expanded.value, + onDismissRequest = { expanded.value = false } + ) { + content(this, expanded) + } +} + +@Composable +fun TopAppBarDialogMenu( + icon: @Composable () -> Unit = {}, + content: @Composable (showing: MutableState) -> Unit = {} +) { + val expanded = remember { mutableStateOf(false) } + + Box( + modifier = Modifier.wrapContentSize(Alignment.TopEnd) + ) { + IconButton( + onClick = { + expanded.value = true + } + ) { + icon() + } + } + + if (expanded.value) { + Dialog( + onDismissRequest = { expanded.value = false }, + content = { content(expanded) } + ) +// AlertDialog( +// modifier = Modifier +// .fillMaxWidth() +// .wrapContentHeight(), +// backgroundColor = MaterialTheme.colorScheme.background, +// onDismissRequest = { expanded.value = false }, +// text = { content(expanded) }, +// buttons = {} +// ) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..7e2ff63 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/SignInDialog.kt @@ -0,0 +1,159 @@ +package com.owenlejeune.tvtime.ui.components + +import android.content.Intent +import android.net.Uri +import android.text.TextUtils +import android.widget.Toast +import androidx.compose.foundation.clickable +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.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.owenlejeune.tvtime.R +import com.owenlejeune.tvtime.extensions.isEmailValid + +@Composable +fun SignInDialog( + showDialog: MutableState, + onSuccess: (success: Boolean) -> Unit +) { + val context = LocalContext.current + + var emailState by rememberSaveable { mutableStateOf("") } + var emailHasErrors by rememberSaveable { mutableStateOf(false) } + var emailError = "" + + var passwordState by rememberSaveable { mutableStateOf("") } + var passwordHasErrors by rememberSaveable { mutableStateOf(false) } + var passwordError = "" + + fun validate(): Boolean { + emailError = "" + 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(passwordState)) { + passwordError = context.getString(R.string.password_empty_error) + } + + emailHasErrors = emailError.isNotEmpty() + passwordHasErrors = passwordError.isNotEmpty() + + return !emailHasErrors && !passwordHasErrors + } + + AlertDialog( + title = { Text(text = stringResource(R.string.action_sign_in)) }, + onDismissRequest = { showDialog.value = false }, + confirmButton = { CancelButton(showDialog = showDialog) }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.sign_in_dialog_message) + ) + ThemedOutlineTextField( + value = emailState, + onValueChange = { + emailHasErrors = false + emailState = it + }, + label = { Text(text = stringResource(R.string.email_label)) }, + isError = emailHasErrors, + errorMessage = emailError + ) + PasswordOutlineTextField( + value = passwordState, + onValueChange = { + passwordHasErrors = false + passwordState = it + }, + label = { Text(text = stringResource(R.string.password_label)) }, + isError = passwordHasErrors, + errorMessage = passwordError + ) + SignInButton(validate = ::validate) { success -> + if (success) { + showDialog.value = false + } else { + Toast.makeText(context, "An error occurred, please try again", Toast.LENGTH_SHORT).show() + } + onSuccess(success) + } + CreateAccountLink() + } + } + ) +} + +@Composable +private fun CancelButton(showDialog: MutableState) { + TextButton(onClick = { showDialog.value = false }) { + Text(text = stringResource(R.string.action_cancel)) + } +} + +@Composable +private fun SignInButton(validate: () -> Boolean, onSuccess: (success: Boolean) -> Unit) { + var signInInProgress by remember { mutableStateOf(false) } + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + if (!signInInProgress) { + if (validate()) { + signInInProgress = true +// signIn(context, emailState, passwordState) { success -> +// signInInProgress = false +// if (success) { +// showDialog.value = false +// } +// } + onSuccess(false) + } + } + } + ) { + if (signInInProgress) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.background, + strokeWidth = 2.dp + ) + } else { + Text(text = stringResource(id = R.string.action_sign_in)) + } + } + +} + +@Composable +private fun CreateAccountLink() { + val context = LocalContext.current + LinkableText( + text = stringResource(R.string.no_account_message), + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .clickable( + onClick = { + val url = "https://www.themoviedb.org/signup" + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(url) + context.startActivity(intent) + } + ) + ) +} \ No newline at end of file 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 df344ec..0513630 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 @@ -6,6 +6,7 @@ import android.widget.Toast import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.* import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester @@ -16,10 +17,17 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Card +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.TextFieldColors +import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -30,21 +38,23 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.BrushPainter -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow @@ -653,21 +663,209 @@ fun AvatarImage( contentDescription = "" ) } else { - Box( + val text = if (author.name.isNotEmpty()) author.name[0] else author.username[0] + RoundedLetterImage( + size = size, + character = text + ) +// Box( +// modifier = Modifier +// .clip(CircleShape) +// .size(size) +// .background(color = MaterialTheme.colorScheme.tertiary) +// ) { +// Text( +// modifier = Modifier +// .fillMaxSize() +// .padding(top = size / 5), +// text = if (author.name.isNotEmpty()) author.name[0].uppercase() else author.username[0].toString(), +// color = MaterialTheme.colorScheme.onTertiary, +// textAlign = TextAlign.Center, +// style = MaterialTheme.typography.titleLarge +// ) +// } + } +} + +@Composable +fun RoundedLetterImage( + size: Dp, + character: Char, + modifier: Modifier = Modifier, + topPadding: Dp = size / 5 +) { + Box( + modifier = Modifier + .clip(CircleShape) + .size(size) + .background(color = MaterialTheme.colorScheme.tertiary) + ) { + Text( modifier = Modifier - .clip(CircleShape) - .size(size) - .background(color = MaterialTheme.colorScheme.tertiary) - ) { + .fillMaxSize() + .padding(top = topPadding), + text = character.uppercase(), + color = MaterialTheme.colorScheme.onTertiary, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + } +} + +@Composable +fun ThemedOutlineTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = androidx.compose.material.LocalTextStyle.current, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + isError: Boolean = false, + errorMessage: String = "", + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = false, + maxLines: Int = Int.MAX_VALUE, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + shape: Shape = androidx.compose.material.MaterialTheme.shapes.small +) { + Column(modifier = modifier) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + enabled = enabled, + readOnly = readOnly, + textStyle = textStyle, + label = label, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = { + if (isError) { + Icon(Icons.Filled.Error, "error", tint = MaterialTheme.colorScheme.error) + } else { + trailingIcon?.invoke() + } + }, + isError = isError, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = singleLine, + maxLines = maxLines, + interactionSource = interactionSource, + shape = shape, + colors = TextFieldDefaults.outlinedTextFieldColors( + unfocusedBorderColor = MaterialTheme.colorScheme.onBackground, + focusedBorderColor = MaterialTheme.colorScheme.primary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + cursorColor = MaterialTheme.colorScheme.primary, + textColor = MaterialTheme.colorScheme.onBackground, + errorBorderColor = MaterialTheme.colorScheme.error, + errorCursorColor = MaterialTheme.colorScheme.error, + errorLabelColor = MaterialTheme.colorScheme.error + ) + ) + if (isError) { Text( - modifier = Modifier - .fillMaxSize() - .padding(top = size / 5), - text = if (author.name.isNotEmpty()) author.name[0].uppercase() else author.username[0].toString(), - color = MaterialTheme.colorScheme.onTertiary, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge + text = errorMessage, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(start = 16.dp) ) } } +} + +@Composable +fun PasswordOutlineTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = androidx.compose.material.LocalTextStyle.current, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + isError: Boolean = false, + errorMessage: String = "", + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = false, + maxLines: Int = Int.MAX_VALUE, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + shape: Shape = androidx.compose.material.MaterialTheme.shapes.small +) { + var passwordVisible by rememberSaveable { mutableStateOf(false) } + ThemedOutlineTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + enabled = enabled, + readOnly = readOnly, + textStyle = textStyle, + label = label, + placeholder = placeholder, + leadingIcon = leadingIcon, + isError = isError, + errorMessage = errorMessage, + keyboardActions = keyboardActions, + singleLine = singleLine, + maxLines = maxLines, + interactionSource = interactionSource, + shape = shape, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + trailingIcon = { + val image = if (passwordVisible) { + Icons.Filled.Visibility + } else { + Icons.Filled.VisibilityOff + } + val description = if (passwordVisible) "Hide password" else "Show password" + IconButton(onClick = { passwordVisible = !passwordVisible } ) { + Icon(imageVector = image, contentDescription = description) + } + } + ) +} + +@Composable +fun LinkableText( + text: String, + modifier: Modifier = Modifier, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current +) { + Text( + text = text, + modifier = modifier, + color = Color(0xFF64B5F6), + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = TextDecoration.Underline, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + onTextLayout = onTextLayout, + style = style + ) } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt index 891a9c5..1a1f80f 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt @@ -16,7 +16,7 @@ sealed class AccountTabNavItem(stringRes: Int, route: String, val mediaType: Med override val name = resourceUtils.getString(stringRes) companion object { - val GuestItems = listOf(RatedMovies, RatedTvShows)//, RatedTvEpisodes) + val GuestItems = listOf(RatedMovies, RatedTvShows, RatedTvEpisodes) } object RatedMovies: AccountTabNavItem(R.string.nav_rated_movies_title, "rated_movies_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession?.ratedMovies ?: emptyList() } ) diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt index d15f0c1..3666c1f 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt @@ -1,5 +1,6 @@ package com.owenlejeune.tvtime.ui.navigation +import androidx.compose.foundation.layout.RowScope import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf @@ -20,8 +21,12 @@ object NavConstants { } @Composable -fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar: MutableState = mutableStateOf(false)) { - NavHost(navController = navController, startDestination = MainNavItem.MainView.route) { +fun MainNavigationRoutes( + navController: NavHostController, + displayUnderStatusBar: MutableState = mutableStateOf(false), + startDestination: String = MainNavItem.MainView.route +) { + NavHost(navController = navController, startDestination = startDestination) { composable(MainNavItem.MainView.route) { displayUnderStatusBar.value = false MainAppView(appNavController = navController) @@ -56,25 +61,31 @@ fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar fun BottomNavigationRoutes( appNavController: NavHostController, navController: NavHostController, - appBarTitle: MutableState + appBarTitle: MutableState, + appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}) ) { NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) { composable(BottomNavItem.Movies.route) { + appBarActions.value = {} MediaTab(appNavController = appNavController, mediaType = MediaViewType.MOVIE) } composable(BottomNavItem.TV.route) { + appBarActions.value = {} MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV) } composable(BottomNavItem.Account.route) { - AccountTab(appBarTitle = appBarTitle, appNavController = appNavController) + AccountTab(appBarTitle = appBarTitle, appNavController = appNavController, appBarActions = appBarActions) } composable(BottomNavItem.People.route) { + appBarActions.value = {} PeopleTab(appBarTitle, appNavController = appNavController) } composable(BottomNavItem.Favourites.route) { + appBarActions.value = {} FavouritesTab() } composable(BottomNavItem.Settings.route) { + appBarActions.value = {} SettingsTab() } } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MainView.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MainView.kt index e7af8b1..a65d76f 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MainView.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MainView.kt @@ -2,13 +2,11 @@ package com.owenlejeune.tvtime.ui.screens import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material.Scaffold import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -40,7 +38,7 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route - val appBarTitle = remember { mutableStateOf(BottomNavItem.Items[0].name) } + val appBarTitle = rememberSaveable { mutableStateOf(BottomNavItem.getByRoute(currentRoute)?.name ?: BottomNavItem.Items[0].name) } val decayAnimationSpec = rememberSplineBasedDecay() val scrollBehavior = remember(decayAnimationSpec) { TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec) @@ -50,6 +48,8 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences val focusSearchBar = remember { mutableStateOf(false) } val searchableScreens = listOf(BottomNavItem.Movies.route, BottomNavItem.TV.route, BottomNavItem.People.route) + val appBarActions = remember { mutableStateOf<@Composable RowScope.() -> Unit>( {} ) } + // todo - scroll state not remember when returing from detail screen Scaffold( @@ -72,7 +72,8 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences } else { TopBar( title = appBarTitle, - scrollBehavior = scrollBehavior + scrollBehavior = scrollBehavior, + appBarActions = appBarActions ) } }, @@ -86,7 +87,7 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences } ) { innerPadding -> Box(modifier = Modifier.padding(innerPadding)) { - BottomNavigationRoutes(appNavController = appNavController, navController = navController, appBarTitle = appBarTitle) + BottomNavigationRoutes(appNavController = appNavController, navController = navController, appBarTitle = appBarTitle, appBarActions = appBarActions) } } } @@ -94,7 +95,8 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences @Composable private fun TopBar( title: MutableState, - scrollBehavior: TopAppBarScrollBehavior + scrollBehavior: TopAppBarScrollBehavior, + appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}) ) { LargeTopAppBar( title = { Text(text = title.value) }, @@ -103,7 +105,8 @@ private fun TopBar( .largeTopAppBarColors( scrolledContainerColor = MaterialTheme.colorScheme.background, titleContentColor = MaterialTheme.colorScheme.primary - ) + ), + actions = appBarActions.value ) } 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 05bd305..f75f197 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 @@ -205,7 +205,7 @@ private fun ActionsView( service = service ) - if (session?.isGuest == false) { + if (session?.isAuthorized == true) { ActionButton( modifier = Modifier.weight(1f), text = stringResource(R.string.add_to_list_action_label), @@ -641,7 +641,7 @@ private fun ReviewsCard( ) }, footer = { - if (SessionManager.currentSession?.isGuest == false) { + if (SessionManager.currentSession?.isAuthorized == true) { Row( modifier = Modifier .fillMaxWidth() 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 f949190..b34d55f 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 @@ -1,14 +1,24 @@ package com.owenlejeune.tvtime.ui.screens.tabs.bottom import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Text +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +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 @@ -17,9 +27,13 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.rememberPagerState +import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.api.tmdb.model.RatedMovie import com.owenlejeune.tvtime.api.tmdb.model.RatedTopLevelMedia import com.owenlejeune.tvtime.api.tmdb.model.RatedTv +import com.owenlejeune.tvtime.ui.components.RoundedLetterImage +import com.owenlejeune.tvtime.ui.components.SignInDialog +import com.owenlejeune.tvtime.ui.components.TopAppBarDropdownMenu import com.owenlejeune.tvtime.ui.navigation.AccountTabNavItem import com.owenlejeune.tvtime.ui.navigation.ListFetchFun import com.owenlejeune.tvtime.ui.navigation.MainNavItem @@ -27,31 +41,53 @@ import com.owenlejeune.tvtime.ui.screens.MediaViewType import com.owenlejeune.tvtime.ui.screens.tabs.top.Tabs import com.owenlejeune.tvtime.utils.SessionManager import com.owenlejeune.tvtime.utils.TmdbUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +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" @OptIn(ExperimentalPagerApi::class) @Composable -fun AccountTab(appNavController: NavHostController, appBarTitle: MutableState) { - if (SessionManager.currentSession?.isGuest == true) { - appBarTitle.value = "Hello, Guest" +fun AccountTab( + appNavController: NavHostController, + 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 = "Not logged in" + appBarTitle.value = stringResource(id = R.string.account_not_logged_in) } - SessionManager.currentSession?.let { session -> - val tabs = if (session.isGuest) { - AccountTabNavItem.GuestItems - } else { - AccountTabNavItem.GuestItems - } + val lastSelectedOption = remember { mutableStateOf("") } - Column { - val pagerState = rememberPagerState() - Tabs(tabs = tabs, pagerState = pagerState) - AccountTabs( - appNavController = appNavController, - tabs = tabs, - pagerState = pagerState - ) + appBarActions.value = { + AccountDropdownMenu(session = SessionManager.currentSession, lastSelectedOption = lastSelectedOption) + } + + if (lastSelectedOption.value.isNotBlank() || lastSelectedOption.value.isBlank()) { + SessionManager.currentSession?.let { session -> + val tabs = if (session.isAuthorized) { + AccountTabNavItem.GuestItems + } else { + AccountTabNavItem.GuestItems + } + + Column { + val pagerState = rememberPagerState() + Tabs(tabs = tabs, pagerState = pagerState) + AccountTabs( + appNavController = appNavController, + tabs = tabs, + pagerState = pagerState + ) + } } } } @@ -64,8 +100,22 @@ fun AccountTabContent( ) { val contentItems = listFetchFun() -// if (contentItems.isNotEmpty() && contentItems[0] is RatedTopLevelMedia) { - LazyColumn(modifier = Modifier.fillMaxSize().padding(12.dp)) { + LazyColumn(modifier = Modifier + .fillMaxSize() + .padding(12.dp)) { + if (contentItems.isEmpty()) { + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 15.dp), + text = stringResource(R.string.no_rated_content_message), + color = MaterialTheme.colorScheme.onBackground, + fontSize = 22.sp, + textAlign = TextAlign.Center + ) + } + } else { items(contentItems.size) { i -> val ratedItem = contentItems[i] as RatedTopLevelMedia @@ -110,16 +160,163 @@ fun AccountTabContent( ) Text( - text = "Rating: ${(ratedItem.rating * 10).toInt()}%", + text = stringResource(id = R.string.rating_test, (ratedItem.rating * 10).toInt()), color = MaterialTheme.colorScheme.onBackground ) } } } -// } + } } } +@Composable +private fun AccountDropdownMenu( + session: SessionManager.Session?, + lastSelectedOption: MutableState +) { + TopAppBarDropdownMenu( + icon = { + when(session?.isAuthorized) { + true -> { } + false -> { GuestSessionIcon() } + null -> { NoSessionAccountIcon() } + } + } + ) { expanded -> + when(session?.isAuthorized) { + true -> { AuthorizedSessionMenuItems(expanded = expanded, lastSelectedOption = lastSelectedOption) } + false -> { GuestSessionMenuItems(expanded = expanded, lastSelectedOption = lastSelectedOption) } + null -> { NoSessionMenuItems(expanded = expanded, lastSelectedOption = lastSelectedOption) } + } + } +} + +@Composable +private fun NoSessionMenuItems( + expanded: MutableState, + lastSelectedOption: MutableState +) { + val showSignInDialog = remember { mutableStateOf(false) } + DropdownMenuItem( + onClick = { + showSignInDialog.value = true + }, + modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceVariant) + ) { + Text(text = stringResource(R.string.action_sign_in), color = MaterialTheme.colorScheme.onSurfaceVariant) + } + + if (showSignInDialog.value) { + SignInDialog(showDialog = showSignInDialog) { success -> + if (success) { + lastSelectedOption.value = NO_SESSION_SIGN_IN + expanded.value = false + } + } + } + + Divider(color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 12.dp)) + + DropdownMenuItem( + onClick = { + createGuestSession(lastSelectedOption) + expanded.value = false + } + ) { + Text(text = stringResource(R.string.action_sign_in_as_guest), color = MaterialTheme.colorScheme.onSurfaceVariant) + } +} + +@Composable +private fun NoSessionAccountIcon() { + Icon( + modifier = Modifier + .size(50.dp) + .padding(end = 8.dp), + imageVector = Icons.Filled.AccountCircle, + contentDescription = stringResource(R.string.account_menu_content_description), + tint = MaterialTheme.colorScheme.primary + ) +} + +@Composable +private fun GuestSessionMenuItems( + expanded: MutableState, + lastSelectedOption: MutableState +) { + val showSignInDialog = remember { mutableStateOf(false) } + DropdownMenuItem( + onClick = { + showSignInDialog.value = true + }, + modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceVariant) + ) { + Text(text = stringResource(id = R.string.action_sign_in), color = MaterialTheme.colorScheme.onSurfaceVariant) + } + + if (showSignInDialog.value) { + SignInDialog(showDialog = showSignInDialog) { success -> + if (success) { + lastSelectedOption.value = GUEST_SIGN_IN + expanded.value = false + } + } + } + + Divider(color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 12.dp)) + + DropdownMenuItem( + onClick = { + signOut(lastSelectedOption) + expanded.value = false + } + ) { + Text(text = stringResource(id = R.string.action_sign_out), color = MaterialTheme.colorScheme.onSurfaceVariant) + } +} + +@Composable +private fun GuestSessionIcon() { + val guestName = stringResource(id = R.string.account_name_guest) + RoundedLetterImage(size = 40.dp, character = guestName[0], modifier = Modifier.padding(end = 8.dp), topPadding = 40.dp / 8) +} + +@Composable +private fun AuthorizedSessionMenuItems( + expanded: MutableState, + lastSelectedOption: MutableState +) { + DropdownMenuItem( + onClick = { + lastSelectedOption.value = ACCOUNT_SIGN_OUT + expanded.value = false + } + ) { + Text(text = stringResource(id = R.string.action_sign_out), color = MaterialTheme.colorScheme.onSurfaceVariant) + } +} + +private fun createGuestSession(lastSelectedOption: MutableState) { + CoroutineScope(Dispatchers.IO).launch { + val session = SessionManager.requestNewGuestSession() + if (session != null) { + withContext(Dispatchers.Main) { + lastSelectedOption.value = NO_SESSION_SIGN_IN_GUEST + } + } + } +} + +private fun signOut(lastSelectedOption: MutableState) { + SessionManager.clearSession { isSuccessful -> + if (isSuccessful) { + lastSelectedOption.value = SIGN_OUT + } + } +} + + @OptIn(ExperimentalPagerApi::class) @Composable fun AccountTabs( 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 66a7550..2f8c521 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt @@ -1,10 +1,7 @@ package com.owenlejeune.tvtime.utils import com.owenlejeune.tvtime.api.tmdb.TmdbClient -import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionBody -import com.owenlejeune.tvtime.api.tmdb.model.RatedEpisode -import com.owenlejeune.tvtime.api.tmdb.model.RatedMovie -import com.owenlejeune.tvtime.api.tmdb.model.RatedTv +import com.owenlejeune.tvtime.api.tmdb.model.* import com.owenlejeune.tvtime.preferences.AppPreferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -27,13 +24,14 @@ object SessionManager: KoinComponent { currentSession?.let { session -> CoroutineScope(Dispatchers.IO).launch { val deleteResponse = authenticationService.deleteSession( - DeleteSessionBody( + SessionBody( session.sessionId ) ) withContext(Dispatchers.Main) { if (deleteResponse.isSuccessful) { _currentSession = null + preferences.guestSessionId = "" } onResponse(deleteResponse.isSuccessful) } @@ -46,6 +44,8 @@ object SessionManager: KoinComponent { val session = GuestSession() session.initialize() _currentSession = session + } else if (preferences.authorizedSessionId.isNotEmpty()) { + } } @@ -58,7 +58,29 @@ object SessionManager: KoinComponent { return _currentSession } - abstract class Session(val sessionId: String, val isGuest: Boolean) { + suspend fun signInWithLogin(email: String, password: String): Boolean { + val service = TmdbClient().createAuthenticationService() + val createTokenResponse = service.createRequestToken() + if (createTokenResponse.isSuccessful) { + createTokenResponse.body()?.let { ctr -> + val body = TokenValidationBody(email, 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 + } + } + } + } + } + return false + } + + abstract class Session(val sessionId: String, val isAuthorized: Boolean) { protected abstract var _ratedMovies: List val ratedMovies: List get() = _ratedMovies @@ -88,7 +110,21 @@ object SessionManager: KoinComponent { abstract suspend fun refresh() } - private class GuestSession: Session(preferences.guestSessionId, true) { + private class AuthorizedSession: Session(preferences.authorizedSessionId, true) { + override var _ratedMovies: List = emptyList() + override var _ratedTvShows: List = emptyList() + override var _ratedTvEpisodes: List = emptyList() + + override suspend fun initialize() { + refresh() + } + + override suspend fun refresh() { + + } + } + + private class GuestSession: Session(preferences.guestSessionId, false) { override var _ratedMovies: List = emptyList() override var _ratedTvShows: List = emptyList() override var _ratedTvEpisodes: List = emptyList() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e600e4a..ea2cc55 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,4 +77,22 @@ Ended Pilot Active + + + Hello, %1$s! + Guest + Not logged in + + No rated content + Rating: $1%d + Sign In as Guest + Account Menu + Sign Out + Email must not be empty + Invalid email + Must enter a password + Sign in using your TMDB credentials + Email + 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 3973fbe..09d5eca 100644 --- a/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Versions.kt +++ b/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Versions.kt @@ -3,7 +3,7 @@ package com.owenlejeune.tvtime.buildsrc object Versions { const val compose = "1.1.0-rc03" - const val compose_material3 = "1.0.0-alpha04" + const val compose_material3 = "1.0.0-alpha06" const val compose_accompanist = "0.22.1-rc" const val compose_navigation = "2.4.0" const val compose_paging = "1.0.0-alpha14"