mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-22 11:40:54 -05:00
add sign in menu to account tab
This commit is contained in:
@@ -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}"
|
||||
|
||||
@@ -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<GuestSessionResponse>
|
||||
|
||||
// @DELETE("authentication/session")
|
||||
@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>
|
||||
}
|
||||
@@ -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<DeleteSessionResponse> {
|
||||
suspend fun deleteSession(body: SessionBody): Response<DeleteSessionResponse> {
|
||||
return service.deleteSession(body)
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
|
||||
|
||||
override suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> {
|
||||
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<RatingResponse> {
|
||||
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)
|
||||
|
||||
@@ -55,7 +55,7 @@ class TvService: KoinComponent, DetailService, HomePageService {
|
||||
|
||||
override suspend fun postRating(id: Int, rating: RatingBody): Response<RatingResponse> {
|
||||
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<RatingResponse> {
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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<Boolean>
|
||||
// var usePreferences: Boolean
|
||||
// get() = preferences.getBoolean(USE_PREFERENCES, false)
|
||||
|
||||
@@ -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 = {}
|
||||
// )
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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() } )
|
||||
|
||||
@@ -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<Boolean> = mutableStateOf(false)) {
|
||||
NavHost(navController = navController, startDestination = MainNavItem.MainView.route) {
|
||||
fun MainNavigationRoutes(
|
||||
navController: NavHostController,
|
||||
displayUnderStatusBar: MutableState<Boolean> = 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<String>
|
||||
appBarTitle: MutableState<String>,
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Float>()
|
||||
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<String>,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<String>) {
|
||||
if (SessionManager.currentSession?.isGuest == true) {
|
||||
appBarTitle.value = "Hello, Guest"
|
||||
fun AccountTab(
|
||||
appNavController: NavHostController,
|
||||
appBarTitle: MutableState<String>,
|
||||
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({})
|
||||
) {
|
||||
if (SessionManager.currentSession?.isAuthorized == false) {
|
||||
appBarTitle.value = stringResource(id = R.string.account_header_title_formatted).replace("%1\$s", stringResource(id = R.string.account_name_guest))
|
||||
} else {
|
||||
appBarTitle.value = "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<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)
|
||||
@Composable
|
||||
fun AccountTabs(
|
||||
|
||||
@@ -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<RatedMovie>
|
||||
val ratedMovies: List<RatedMovie>
|
||||
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<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 _ratedTvShows: List<RatedTv> = emptyList()
|
||||
override var _ratedTvEpisodes: List<RatedEpisode> = emptyList()
|
||||
|
||||
@@ -77,4 +77,22 @@
|
||||
<string name="status_ended">Ended</string>
|
||||
<string name="status_pilot">Pilot</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>
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user