add sign in menu to account tab

This commit is contained in:
Owen LeJeune
2022-03-08 21:26:42 -05:00
parent 6ce7f3fa5c
commit 0ed5fa88f1
22 changed files with 824 additions and 76 deletions

View File

@@ -59,6 +59,7 @@ dependencies {
implementation "androidx.compose.ui:ui:${Versions.compose}" implementation "androidx.compose.ui:ui:${Versions.compose}"
implementation "androidx.compose.material3:material3:${Versions.compose_material3}" implementation "androidx.compose.material3:material3:${Versions.compose_material3}"
implementation "androidx.compose.material:material:${Versions.compose}" 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.compose.ui:ui-tooling-preview:${Versions.compose}"
implementation "androidx.activity:activity-compose:${Versions.activity_compose}" implementation "androidx.activity:activity-compose:${Versions.activity_compose}"
implementation "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}" implementation "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}"

View File

@@ -1,21 +1,26 @@
package com.owenlejeune.tvtime.api.tmdb package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionBody import com.owenlejeune.tvtime.api.tmdb.model.*
import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionResponse
import com.owenlejeune.tvtime.api.tmdb.model.GuestSessionResponse
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.HTTP import retrofit2.http.HTTP
import retrofit2.http.POST
interface AuthenticationApi { interface AuthenticationApi {
@GET("authentication/guest_session/new") @GET("authentication/guest_session/new")
suspend fun getNewGuestSession(): Response<GuestSessionResponse> suspend fun getNewGuestSession(): Response<GuestSessionResponse>
// @DELETE("authentication/session")
@HTTP(method = "DELETE", path = "authentication/session", hasBody = true) @HTTP(method = "DELETE", path = "authentication/session", hasBody = true)
suspend fun deleteSession(@Body body: DeleteSessionBody): Response<DeleteSessionResponse> suspend fun deleteSession(@Body body: SessionBody): Response<DeleteSessionResponse>
@GET("authentication/token/new")
suspend fun createRequestToken(): Response<CreateTokenResponse>
@POST("authentication/session/new")
suspend fun createSession(@Body body: SessionBody): Response<CreateSessionResponse>
@POST("authentication/token/validate_with_login")
suspend fun validateTokenWithLogin(@Body body: TokenValidationBody): Response<CreateTokenResponse>
} }

View File

@@ -1,6 +1,6 @@
package com.owenlejeune.tvtime.api.tmdb 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.DeleteSessionResponse
import com.owenlejeune.tvtime.api.tmdb.model.GuestSessionResponse import com.owenlejeune.tvtime.api.tmdb.model.GuestSessionResponse
import retrofit2.Response import retrofit2.Response
@@ -13,7 +13,7 @@ class AuthenticationService {
return service.getNewGuestSession() return service.getNewGuestSession()
} }
suspend fun deleteSession(body: DeleteSessionBody): Response<DeleteSessionResponse> { suspend fun deleteSession(body: SessionBody): Response<DeleteSessionResponse> {
return service.deleteSession(body) return service.deleteSession(body)
} }

View File

@@ -55,7 +55,7 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
override suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> { override suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> {
val session = SessionManager.currentSession ?: throw Exception("Session must not be null") 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) movieService.postMovieRatingAsGuest(id, session.sessionId, rating)
} else { } else {
movieService.postMovieRatingAsUser(id, session.sessionId, rating) movieService.postMovieRatingAsUser(id, session.sessionId, rating)
@@ -64,7 +64,7 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
override suspend fun deleteRating(id: Int): Response<RatingResponse> { override suspend fun deleteRating(id: Int): Response<RatingResponse> {
val session = SessionManager.currentSession ?: throw Exception("Session must not be null") val session = SessionManager.currentSession ?: throw Exception("Session must not be null")
return if (session.isGuest) { return if (!session.isAuthorized) {
movieService.deleteMovieReviewAsGuest(id, session.sessionId) movieService.deleteMovieReviewAsGuest(id, session.sessionId)
} else { } else {
movieService.deleteMovieReviewAsUser(id, session.sessionId) movieService.deleteMovieReviewAsUser(id, session.sessionId)

View File

@@ -55,7 +55,7 @@ class TvService: KoinComponent, DetailService, HomePageService {
override suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> { override suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> {
val session = SessionManager.currentSession ?: throw Exception("Session must not be null") 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) service.postTvRatingAsGuest(id, session.sessionId, rating)
} else { } else {
service.postTvRatingAsUser(id, session.sessionId, rating) service.postTvRatingAsUser(id, session.sessionId, rating)
@@ -64,7 +64,7 @@ class TvService: KoinComponent, DetailService, HomePageService {
override suspend fun deleteRating(id: Int): Response<RatingResponse> { override suspend fun deleteRating(id: Int): Response<RatingResponse> {
val session = SessionManager.currentSession ?: throw Exception("Session must not be null") val session = SessionManager.currentSession ?: throw Exception("Session must not be null")
return if (session.isGuest) { return if (!session.isAuthorized) {
service.deleteTvReviewAsGuest(id, session.sessionId) service.deleteTvReviewAsGuest(id, session.sessionId)
} else { } else {
service.deleteTvReviewAsUser(id, session.sessionId) service.deleteTvReviewAsUser(id, session.sessionId)

View File

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

View File

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

View File

@@ -2,6 +2,6 @@ package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
class DeleteSessionBody( class SessionBody(
@SerializedName("session_id") val sessionsId: String @SerializedName("session_id") val sessionsId: String
) )

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ class AppPreferences(context: Context) {
private val PERSISTENT_SEARCH = "persistent_search" private val PERSISTENT_SEARCH = "persistent_search"
private val HIDE_TITLE = "hide_title" private val HIDE_TITLE = "hide_title"
private val GUEST_SESSION = "guest_session_id" private val GUEST_SESSION = "guest_session_id"
private val AUTHORIZED_SESSION = "authorized_session_id"
} }
private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE) private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE)
@@ -27,6 +28,10 @@ class AppPreferences(context: Context) {
var guestSessionId: String var guestSessionId: String
get() = preferences.getString(GUEST_SESSION, "") ?: "" get() = preferences.getString(GUEST_SESSION, "") ?: ""
set(value) { preferences.put(GUEST_SESSION, value) } 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<Boolean> // val usePreferences: MutableState<Boolean>
// var usePreferences: Boolean // var usePreferences: Boolean
// get() = preferences.getBoolean(USE_PREFERENCES, false) // get() = preferences.getBoolean(USE_PREFERENCES, false)

View File

@@ -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<Boolean>) -> 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<Boolean>) -> 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 = {}
// )
}
}

View File

@@ -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<Boolean>,
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<Boolean>) {
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)
}
)
)
}

View File

@@ -6,6 +6,7 @@ import android.widget.Toast
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.BringIntoViewRequester
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.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Card 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.Icons
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Search 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.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier 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.focus.onFocusEvent
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.painter.BrushPainter import androidx.compose.ui.graphics.painter.BrushPainter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle 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.FontFamily
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight 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.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -653,6 +663,37 @@ fun AvatarImage(
contentDescription = "" contentDescription = ""
) )
} else { } else {
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( Box(
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
@@ -662,12 +703,169 @@ fun AvatarImage(
Text( Text(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(top = size / 5), .padding(top = topPadding),
text = if (author.name.isNotEmpty()) author.name[0].uppercase() else author.username[0].toString(), text = character.uppercase(),
color = MaterialTheme.colorScheme.onTertiary, color = MaterialTheme.colorScheme.onTertiary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge 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(
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
)
} }

View File

@@ -16,7 +16,7 @@ sealed class AccountTabNavItem(stringRes: Int, route: String, val mediaType: Med
override val name = resourceUtils.getString(stringRes) override val name = resourceUtils.getString(stringRes)
companion object { 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() } ) object RatedMovies: AccountTabNavItem(R.string.nav_rated_movies_title, "rated_movies_route", MediaViewType.MOVIE, screenContent, { SessionManager.currentSession?.ratedMovies ?: emptyList() } )

View File

@@ -1,5 +1,6 @@
package com.owenlejeune.tvtime.ui.navigation package com.owenlejeune.tvtime.ui.navigation
import androidx.compose.foundation.layout.RowScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -20,8 +21,12 @@ object NavConstants {
} }
@Composable @Composable
fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar: MutableState<Boolean> = mutableStateOf(false)) { fun MainNavigationRoutes(
NavHost(navController = navController, startDestination = MainNavItem.MainView.route) { navController: NavHostController,
displayUnderStatusBar: MutableState<Boolean> = mutableStateOf(false),
startDestination: String = MainNavItem.MainView.route
) {
NavHost(navController = navController, startDestination = startDestination) {
composable(MainNavItem.MainView.route) { composable(MainNavItem.MainView.route) {
displayUnderStatusBar.value = false displayUnderStatusBar.value = false
MainAppView(appNavController = navController) MainAppView(appNavController = navController)
@@ -56,25 +61,31 @@ fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar
fun BottomNavigationRoutes( fun BottomNavigationRoutes(
appNavController: NavHostController, appNavController: NavHostController,
navController: NavHostController, navController: NavHostController,
appBarTitle: MutableState<String> appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({})
) { ) {
NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) { NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) {
composable(BottomNavItem.Movies.route) { composable(BottomNavItem.Movies.route) {
appBarActions.value = {}
MediaTab(appNavController = appNavController, mediaType = MediaViewType.MOVIE) MediaTab(appNavController = appNavController, mediaType = MediaViewType.MOVIE)
} }
composable(BottomNavItem.TV.route) { composable(BottomNavItem.TV.route) {
appBarActions.value = {}
MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV) MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV)
} }
composable(BottomNavItem.Account.route) { composable(BottomNavItem.Account.route) {
AccountTab(appBarTitle = appBarTitle, appNavController = appNavController) AccountTab(appBarTitle = appBarTitle, appNavController = appNavController, appBarActions = appBarActions)
} }
composable(BottomNavItem.People.route) { composable(BottomNavItem.People.route) {
appBarActions.value = {}
PeopleTab(appBarTitle, appNavController = appNavController) PeopleTab(appBarTitle, appNavController = appNavController)
} }
composable(BottomNavItem.Favourites.route) { composable(BottomNavItem.Favourites.route) {
appBarActions.value = {}
FavouritesTab() FavouritesTab()
} }
composable(BottomNavItem.Settings.route) { composable(BottomNavItem.Settings.route) {
appBarActions.value = {}
SettingsTab() SettingsTab()
} }
} }

View File

@@ -2,13 +2,11 @@ package com.owenlejeune.tvtime.ui.screens
import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@@ -40,7 +38,7 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route 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<Float>() val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val scrollBehavior = remember(decayAnimationSpec) { val scrollBehavior = remember(decayAnimationSpec) {
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec) TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec)
@@ -50,6 +48,8 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences
val focusSearchBar = remember { mutableStateOf(false) } val focusSearchBar = remember { mutableStateOf(false) }
val searchableScreens = listOf(BottomNavItem.Movies.route, BottomNavItem.TV.route, BottomNavItem.People.route) 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 // todo - scroll state not remember when returing from detail screen
Scaffold( Scaffold(
@@ -72,7 +72,8 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences
} else { } else {
TopBar( TopBar(
title = appBarTitle, title = appBarTitle,
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior,
appBarActions = appBarActions
) )
} }
}, },
@@ -86,7 +87,7 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences
} }
) { innerPadding -> ) { innerPadding ->
Box(modifier = Modifier.padding(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 @Composable
private fun TopBar( private fun TopBar(
title: MutableState<String>, title: MutableState<String>,
scrollBehavior: TopAppBarScrollBehavior scrollBehavior: TopAppBarScrollBehavior,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({})
) { ) {
LargeTopAppBar( LargeTopAppBar(
title = { Text(text = title.value) }, title = { Text(text = title.value) },
@@ -103,7 +105,8 @@ private fun TopBar(
.largeTopAppBarColors( .largeTopAppBarColors(
scrolledContainerColor = MaterialTheme.colorScheme.background, scrolledContainerColor = MaterialTheme.colorScheme.background,
titleContentColor = MaterialTheme.colorScheme.primary titleContentColor = MaterialTheme.colorScheme.primary
) ),
actions = appBarActions.value
) )
} }

View File

@@ -205,7 +205,7 @@ private fun ActionsView(
service = service service = service
) )
if (session?.isGuest == false) { if (session?.isAuthorized == true) {
ActionButton( ActionButton(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
text = stringResource(R.string.add_to_list_action_label), text = stringResource(R.string.add_to_list_action_label),
@@ -641,7 +641,7 @@ private fun ReviewsCard(
) )
}, },
footer = { footer = {
if (SessionManager.currentSession?.isGuest == false) { if (SessionManager.currentSession?.isAuthorized == true) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@@ -1,14 +1,24 @@
package com.owenlejeune.tvtime.ui.screens.tabs.bottom package com.owenlejeune.tvtime.ui.screens.tabs.bottom
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn 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.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController 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.HorizontalPager
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState 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.RatedMovie
import com.owenlejeune.tvtime.api.tmdb.model.RatedTopLevelMedia import com.owenlejeune.tvtime.api.tmdb.model.RatedTopLevelMedia
import com.owenlejeune.tvtime.api.tmdb.model.RatedTv 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.AccountTabNavItem
import com.owenlejeune.tvtime.ui.navigation.ListFetchFun import com.owenlejeune.tvtime.ui.navigation.ListFetchFun
import com.owenlejeune.tvtime.ui.navigation.MainNavItem import com.owenlejeune.tvtime.ui.navigation.MainNavItem
@@ -27,18 +41,39 @@ import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.tabs.top.Tabs import com.owenlejeune.tvtime.ui.screens.tabs.top.Tabs
import com.owenlejeune.tvtime.utils.SessionManager import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.utils.TmdbUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.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) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun AccountTab(appNavController: NavHostController, appBarTitle: MutableState<String>) { fun AccountTab(
if (SessionManager.currentSession?.isGuest == true) { appNavController: NavHostController,
appBarTitle.value = "Hello, Guest" 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 { } else {
appBarTitle.value = "Not logged in" appBarTitle.value = stringResource(id = R.string.account_not_logged_in)
} }
val lastSelectedOption = remember { mutableStateOf("") }
appBarActions.value = {
AccountDropdownMenu(session = SessionManager.currentSession, lastSelectedOption = lastSelectedOption)
}
if (lastSelectedOption.value.isNotBlank() || lastSelectedOption.value.isBlank()) {
SessionManager.currentSession?.let { session -> SessionManager.currentSession?.let { session ->
val tabs = if (session.isGuest) { val tabs = if (session.isAuthorized) {
AccountTabNavItem.GuestItems AccountTabNavItem.GuestItems
} else { } else {
AccountTabNavItem.GuestItems AccountTabNavItem.GuestItems
@@ -55,6 +90,7 @@ fun AccountTab(appNavController: NavHostController, appBarTitle: MutableState<St
} }
} }
} }
}
@Composable @Composable
fun AccountTabContent( fun AccountTabContent(
@@ -64,8 +100,22 @@ fun AccountTabContent(
) { ) {
val contentItems = listFetchFun() val contentItems = listFetchFun()
// if (contentItems.isNotEmpty() && contentItems[0] is RatedTopLevelMedia) { LazyColumn(modifier = Modifier
LazyColumn(modifier = Modifier.fillMaxSize().padding(12.dp)) { .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 -> items(contentItems.size) { i ->
val ratedItem = contentItems[i] as RatedTopLevelMedia val ratedItem = contentItems[i] as RatedTopLevelMedia
@@ -110,15 +160,162 @@ fun AccountTabContent(
) )
Text( Text(
text = "Rating: ${(ratedItem.rating * 10).toInt()}%", text = stringResource(id = R.string.rating_test, (ratedItem.rating * 10).toInt()),
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
) )
} }
} }
} }
// }
} }
} }
}
@Composable
private fun AccountDropdownMenu(
session: SessionManager.Session?,
lastSelectedOption: MutableState<String>
) {
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<Boolean>,
lastSelectedOption: MutableState<String>
) {
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<Boolean>,
lastSelectedOption: MutableState<String>
) {
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<Boolean>,
lastSelectedOption: MutableState<String>
) {
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<String>) {
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<String>) {
SessionManager.clearSession { isSuccessful ->
if (isSuccessful) {
lastSelectedOption.value = SIGN_OUT
}
}
}
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable

View File

@@ -1,10 +1,7 @@
package com.owenlejeune.tvtime.utils package com.owenlejeune.tvtime.utils
import com.owenlejeune.tvtime.api.tmdb.TmdbClient import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.model.DeleteSessionBody import com.owenlejeune.tvtime.api.tmdb.model.*
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.preferences.AppPreferences import com.owenlejeune.tvtime.preferences.AppPreferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -27,13 +24,14 @@ object SessionManager: KoinComponent {
currentSession?.let { session -> currentSession?.let { session ->
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val deleteResponse = authenticationService.deleteSession( val deleteResponse = authenticationService.deleteSession(
DeleteSessionBody( SessionBody(
session.sessionId session.sessionId
) )
) )
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (deleteResponse.isSuccessful) { if (deleteResponse.isSuccessful) {
_currentSession = null _currentSession = null
preferences.guestSessionId = ""
} }
onResponse(deleteResponse.isSuccessful) onResponse(deleteResponse.isSuccessful)
} }
@@ -46,6 +44,8 @@ object SessionManager: KoinComponent {
val session = GuestSession() val session = GuestSession()
session.initialize() session.initialize()
_currentSession = session _currentSession = session
} else if (preferences.authorizedSessionId.isNotEmpty()) {
} }
} }
@@ -58,7 +58,29 @@ object SessionManager: KoinComponent {
return _currentSession 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<RatedMovie> protected abstract var _ratedMovies: List<RatedMovie>
val ratedMovies: List<RatedMovie> val ratedMovies: List<RatedMovie>
get() = _ratedMovies get() = _ratedMovies
@@ -88,7 +110,21 @@ object SessionManager: KoinComponent {
abstract suspend fun refresh() abstract suspend fun refresh()
} }
private class GuestSession: Session(preferences.guestSessionId, true) { private class AuthorizedSession: Session(preferences.authorizedSessionId, true) {
override var _ratedMovies: List<RatedMovie> = emptyList()
override var _ratedTvShows: List<RatedTv> = emptyList()
override var _ratedTvEpisodes: List<RatedEpisode> = emptyList()
override suspend fun initialize() {
refresh()
}
override suspend fun refresh() {
}
}
private class GuestSession: Session(preferences.guestSessionId, false) {
override var _ratedMovies: List<RatedMovie> = emptyList() override var _ratedMovies: List<RatedMovie> = emptyList()
override var _ratedTvShows: List<RatedTv> = emptyList() override var _ratedTvShows: List<RatedTv> = emptyList()
override var _ratedTvEpisodes: List<RatedEpisode> = emptyList() override var _ratedTvEpisodes: List<RatedEpisode> = emptyList()

View File

@@ -77,4 +77,22 @@
<string name="status_ended">Ended</string> <string name="status_ended">Ended</string>
<string name="status_pilot">Pilot</string> <string name="status_pilot">Pilot</string>
<string name="status_active">Active</string> <string name="status_active">Active</string>
<!-- account -->
<string name="account_header_title_formatted">Hello, %1$s!</string>
<string name="account_name_guest">Guest</string>
<string name="account_not_logged_in">Not logged in</string>
<string name="no_rated_content_message">No rated content</string>
<string name="rating_test">Rating: $1%d</string>
<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="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="password_label">Password</string>
<string name="no_account_message">Don\'t have an account?</string>
</resources> </resources>

View File

@@ -3,7 +3,7 @@ package com.owenlejeune.tvtime.buildsrc
object Versions { object Versions {
const val compose = "1.1.0-rc03" 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_accompanist = "0.22.1-rc"
const val compose_navigation = "2.4.0" const val compose_navigation = "2.4.0"
const val compose_paging = "1.0.0-alpha14" const val compose_paging = "1.0.0-alpha14"