refactor onboarding
This commit is contained in:
@ -3,10 +3,11 @@ import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
kotlin("android")
|
||||
alias(libs.plugins.org.jetbrains.kotlin.android)
|
||||
id("dagger.hilt.android.plugin")
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.apollo)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("app/external/keystore.properties")
|
||||
@ -90,9 +91,7 @@ android {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get()
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += listOf("/META-INF/{AL2.0,LGPL2.1}")
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
package app.omnivore.omnivore.core.designsystem.component
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun DividerWithText(text: String) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp),
|
||||
thickness = 1.dp,
|
||||
color = Color.Black
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
color = Color.Black,
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.width(50.dp)
|
||||
.padding(start = 8.dp),
|
||||
thickness = 1.dp,
|
||||
color = Color.Black
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package app.omnivore.omnivore.core.designsystem.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val OmnivoreBrand = Color(0xFFFCEBA8)
|
||||
@ -1,96 +0,0 @@
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import app.omnivore.omnivore.utils.AppleConstants
|
||||
import app.omnivore.omnivore.R
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
fun AppleAuthButton(viewModel: LoginViewModel) {
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
|
||||
LoadingButtonWithIcon(
|
||||
text = stringResource(R.string.apple_auth_text),
|
||||
loadingText = stringResource(R.string.apple_auth_loading),
|
||||
isLoading = viewModel.isLoading,
|
||||
icon = painterResource(id = R.drawable.ic_logo_apple),
|
||||
modifier = Modifier.padding(vertical = 6.dp),
|
||||
onClick = { showDialog.value = true }
|
||||
)
|
||||
|
||||
if (showDialog.value) {
|
||||
AppleAuthDialog(onDismiss = { token ->
|
||||
if (token != null ) {
|
||||
viewModel.handleAppleToken(token)
|
||||
}
|
||||
showDialog.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppleAuthDialog(onDismiss: (String?) -> Unit) {
|
||||
Dialog(onDismissRequest = { onDismiss(null) }) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = Color.White
|
||||
) {
|
||||
AppleAuthWebView(onDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
fun AppleAuthWebView(onDismiss: (String?) -> Unit) {
|
||||
val url = AppleConstants.authUrl +
|
||||
"?client_id=" + AppleConstants.clientId +
|
||||
"&redirect_uri=" + URLEncoder.encode(AppleConstants.redirectURI, "utf8") +
|
||||
"&response_type=code%20id_token" +
|
||||
"&scope=" + AppleConstants.scope +
|
||||
"&response_mode=form_post" +
|
||||
"&state=android:login"
|
||||
|
||||
// Adding a WebView inside AndroidView
|
||||
// with layout as full screen
|
||||
AndroidView(factory = {
|
||||
WebView(it).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||
if (request?.url.toString().contains("android-apple-token")) {
|
||||
val uri = Uri.parse(request!!.url.toString())
|
||||
val token = uri.getQueryParameter("token")
|
||||
|
||||
onDismiss(token)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
settings.javaScriptEnabled = true
|
||||
loadUrl(url)
|
||||
}
|
||||
}, update = {
|
||||
it.loadUrl(url)
|
||||
})
|
||||
}
|
||||
@ -1,159 +0,0 @@
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
fun CreateUserProfileView(viewModel: LoginViewModel) {
|
||||
var name by rememberSaveable { mutableStateOf("") }
|
||||
var username by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.create_user_profile_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
UserProfileFields(
|
||||
name = name,
|
||||
username = username,
|
||||
usernameValidationErrorMessage = viewModel.usernameValidationErrorMessage,
|
||||
showUsernameAsAvailable = viewModel.hasValidUsername,
|
||||
onNameChange = { name = it },
|
||||
onUsernameChange = {
|
||||
username = it
|
||||
viewModel.validateUsername(it)
|
||||
},
|
||||
onSubmit = { viewModel.submitProfile(username = username, name = name) }
|
||||
)
|
||||
|
||||
// TODO: add a activity indicator (maybe after a delay?)
|
||||
if (viewModel.isLoading) {
|
||||
Text(stringResource(R.string.create_user_profile_loading))
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.create_user_profile_action_cancel)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.cancelNewUserSignUp() }
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserProfileFields(
|
||||
name: String,
|
||||
username: String,
|
||||
usernameValidationErrorMessage: String?,
|
||||
showUsernameAsAvailable: Boolean,
|
||||
onNameChange: (String) -> Unit,
|
||||
onUsernameChange: (String) -> Unit,
|
||||
onSubmit: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(25.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
placeholder = { Text(stringResource(R.string.create_user_profile_field_placeholder_name)) },
|
||||
label = { Text(stringResource(R.string.create_user_profile_field_label_name)) },
|
||||
onValueChange = onNameChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
placeholder = { Text(stringResource(R.string.create_user_profile_field_placeholder_username)) },
|
||||
label = { Text(stringResource(R.string.create_user_profile_field_label_username)) },
|
||||
onValueChange = onUsernameChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
trailingIcon = {
|
||||
if (showUsernameAsAvailable) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.CheckCircle,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (usernameValidationErrorMessage != null) {
|
||||
Text(
|
||||
text = usernameValidationErrorMessage,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (name.isNotBlank() && username.isNotBlank()) {
|
||||
onSubmit()
|
||||
focusManager.clearFocus()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.create_user_profile_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D),
|
||||
containerColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.create_user_profile_action_submit),
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,171 +0,0 @@
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.feature.auth.AuthUtils.autofill
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
fun EmailLoginView(viewModel: LoginViewModel) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
var email by rememberSaveable { mutableStateOf("") }
|
||||
var password by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
LoginFields(
|
||||
email,
|
||||
password,
|
||||
onEmailChange = { email = it },
|
||||
onPasswordChange = { password = it },
|
||||
onLoginClick = { viewModel.login(email, password) }
|
||||
)
|
||||
|
||||
// TODO: add a activity indicator (maybe after a delay?)
|
||||
if (viewModel.isLoading) {
|
||||
Text(stringResource(R.string.email_login_loading))
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.email_login_action_back)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showSocialLogin() }
|
||||
)
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.email_login_action_no_account)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showEmailSignUp() }
|
||||
)
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.email_login_action_forgot_password)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = {
|
||||
val uri = "${BuildConfig.OMNIVORE_WEB_URL}/auth/forgot-password"
|
||||
uriHandler.openUri(uri)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LoginFields(
|
||||
email: String,
|
||||
password: String,
|
||||
onEmailChange: (String) -> Unit,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
onLoginClick: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(25.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.autofill(
|
||||
autofillTypes = listOf(
|
||||
AutofillType.EmailAddress,
|
||||
),
|
||||
onFill = { onEmailChange(it) }
|
||||
),
|
||||
value = email,
|
||||
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_email)) },
|
||||
label = { Text(stringResource(R.string.email_login_field_label_email)) },
|
||||
onValueChange = onEmailChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Email,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.autofill(
|
||||
autofillTypes = listOf(
|
||||
AutofillType.Password,
|
||||
),
|
||||
onFill = { onPasswordChange(it) }
|
||||
),
|
||||
value = password,
|
||||
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_password)) },
|
||||
label = { Text(stringResource(R.string.email_login_field_label_password)) },
|
||||
onValueChange = onPasswordChange,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Password,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (email.isNotBlank() && password.isNotBlank()) {
|
||||
onLoginClick()
|
||||
focusManager.clearFocus()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.email_login_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D),
|
||||
containerColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.email_login_action_login),
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,264 +0,0 @@
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
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
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.feature.auth.AuthUtils.autofill
|
||||
|
||||
@Composable
|
||||
fun EmailSignUpView(viewModel: LoginViewModel) {
|
||||
if (viewModel.pendingEmailUserCreds != null) {
|
||||
val email = viewModel.pendingEmailUserCreds?.email ?: ""
|
||||
val password = viewModel.pendingEmailUserCreds?.password ?: ""
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.email_signup_verification_message, email),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Button(onClick = {
|
||||
viewModel.login(email, password)
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D),
|
||||
containerColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.email_signup_check_status),
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
)
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.email_signup_action_use_different_email)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showEmailSignUp() }
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EmailSignUpForm(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
fun EmailSignUpForm(viewModel: LoginViewModel) {
|
||||
var email by rememberSaveable { mutableStateOf("") }
|
||||
var password by rememberSaveable { mutableStateOf("") }
|
||||
var name by rememberSaveable { mutableStateOf("") }
|
||||
var username by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
EmailSignUpFields(
|
||||
email = email,
|
||||
password = password,
|
||||
name = name,
|
||||
username = username,
|
||||
usernameValidationErrorMessage = viewModel.usernameValidationErrorMessage,
|
||||
showUsernameAsAvailable = viewModel.hasValidUsername,
|
||||
onEmailChange = { email = it },
|
||||
onPasswordChange = { password = it },
|
||||
onNameChange = { name = it },
|
||||
onUsernameChange = {
|
||||
username = it
|
||||
viewModel.validateUsername(it)
|
||||
},
|
||||
onSubmit = {
|
||||
viewModel.submitEmailSignUp(
|
||||
email = email,
|
||||
password = password,
|
||||
username = username,
|
||||
name = name
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// TODO: add a activity indicator (maybe after a delay?)
|
||||
if (viewModel.isLoading) {
|
||||
Text(stringResource(R.string.email_signup_loading))
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.email_signup_action_back)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showSocialLogin() }
|
||||
)
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.email_signup_action_already_have_account)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showEmailSignIn() }
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun EmailSignUpFields(
|
||||
email: String,
|
||||
password: String,
|
||||
name: String,
|
||||
username: String,
|
||||
usernameValidationErrorMessage: String?,
|
||||
showUsernameAsAvailable: Boolean,
|
||||
onEmailChange: (String) -> Unit,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
onNameChange: (String) -> Unit,
|
||||
onUsernameChange: (String) -> Unit,
|
||||
onSubmit: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(25.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.autofill(
|
||||
autofillTypes = listOf(
|
||||
AutofillType.EmailAddress,
|
||||
),
|
||||
onFill = { onEmailChange(it) }
|
||||
),
|
||||
value = email,
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_email)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_email)) },
|
||||
onValueChange = onEmailChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.autofill(
|
||||
autofillTypes = listOf(
|
||||
AutofillType.Password,
|
||||
),
|
||||
onFill = { onPasswordChange(it) }
|
||||
),
|
||||
value = password,
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_password)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_password)) },
|
||||
onValueChange = onPasswordChange,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_name)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_name)) },
|
||||
onValueChange = onNameChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_username)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_username)) },
|
||||
onValueChange = onUsernameChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
trailingIcon = {
|
||||
if (showUsernameAsAvailable) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.CheckCircle,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (usernameValidationErrorMessage != null) {
|
||||
Text(
|
||||
text = usernameValidationErrorMessage,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button(onClick = {
|
||||
if (email.isNotBlank() && password.isNotBlank() && username.isNotBlank() && name.isNotBlank()) {
|
||||
onSubmit()
|
||||
focusManager.clearFocus()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.email_signup_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D),
|
||||
containerColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.email_signup_action_sign_up),
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.R
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
||||
import com.google.android.gms.tasks.Task
|
||||
|
||||
@Composable
|
||||
fun GoogleAuthButton(viewModel: LoginViewModel) {
|
||||
val context = LocalContext.current
|
||||
|
||||
|
||||
val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
||||
.requestIdToken(BuildConfig.OMNIVORE_GAUTH_SERVER_CLIENT_ID)
|
||||
.requestEmail()
|
||||
.build()
|
||||
|
||||
val startForResult =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val intent = result.data
|
||||
if (result.data != null) {
|
||||
val task: Task<GoogleSignInAccount> = GoogleSignIn.getSignedInAccountFromIntent(intent)
|
||||
viewModel.handleGoogleAuthTask(task)
|
||||
}
|
||||
} else {
|
||||
viewModel.showGoogleErrorMessage()
|
||||
}
|
||||
}
|
||||
|
||||
LoadingButtonWithIcon(
|
||||
text = stringResource(R.string.google_auth_text),
|
||||
loadingText = stringResource(R.string.google_auth_loading),
|
||||
isLoading = viewModel.isLoading,
|
||||
icon = painterResource(id = R.drawable.ic_logo_google),
|
||||
onClick = {
|
||||
val googleSignIn = GoogleSignIn.getClient(context, signInOptions)
|
||||
|
||||
googleSignIn.silentSignIn()
|
||||
.addOnCompleteListener { task ->
|
||||
if (task.isSuccessful) {
|
||||
viewModel.handleGoogleAuthTask(task)
|
||||
} else {
|
||||
startForResult.launch(googleSignIn.signInIntent)
|
||||
}
|
||||
}
|
||||
.addOnFailureListener {
|
||||
startForResult.launch(googleSignIn.signInIntent)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun LoadingButtonWithIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
loadingText: String,
|
||||
icon: Painter,
|
||||
isLoading: Boolean = false,
|
||||
shape: Shape = Shapes().medium,
|
||||
borderColor: Color = Color.LightGray,
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||
progressIndicatorColor: Color = MaterialTheme.colorScheme.primary,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier.clickable(
|
||||
enabled = !isLoading,
|
||||
onClick = onClick
|
||||
),
|
||||
shape = shape,
|
||||
border = BorderStroke(width = 1.dp, color = borderColor),
|
||||
color = backgroundColor
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 16.dp,
|
||||
top = 12.dp,
|
||||
bottom = 12.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = "SignInButton",
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(text = if (isLoading) loadingText else text)
|
||||
if (isLoading) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(16.dp)
|
||||
.width(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = progressIndicatorColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,174 +0,0 @@
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
|
||||
@Composable
|
||||
fun WelcomeScreen(viewModel: LoginViewModel) {
|
||||
OmnivoreTheme(darkTheme = false) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
WelcomeScreenContent(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
fun WelcomeScreenContent(viewModel: LoginViewModel) {
|
||||
val registrationState: RegistrationState by viewModel.registrationStateLiveData.observeAsState(
|
||||
RegistrationState.SocialLogin
|
||||
)
|
||||
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
snackbarHost = { SnackbarHost(hostState = snackBarHostState) },
|
||||
containerColor = Color(0xFFFCEBA8)
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceAround,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_omnivore_name_logo),
|
||||
contentDescription = "Omnivore Icon with Name"
|
||||
)
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
|
||||
when (registrationState) {
|
||||
RegistrationState.EmailSignIn -> {
|
||||
EmailLoginView(viewModel = viewModel)
|
||||
}
|
||||
|
||||
RegistrationState.EmailSignUp -> {
|
||||
EmailSignUpView(viewModel = viewModel)
|
||||
}
|
||||
|
||||
RegistrationState.SelfHosted -> {
|
||||
SelfHostedView(viewModel = viewModel)
|
||||
}
|
||||
|
||||
RegistrationState.SocialLogin -> {
|
||||
Text(
|
||||
text = stringResource(id = R.string.welcome_title),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.headlineLarge
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.welcome_subtitle),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
|
||||
MoreInfoButton()
|
||||
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
|
||||
AuthProviderView(viewModel = viewModel)
|
||||
}
|
||||
|
||||
RegistrationState.PendingUser -> {
|
||||
CreateUserProfileView(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(viewModel.errorMessage) {
|
||||
viewModel.errorMessage?.let { message ->
|
||||
val result = snackBarHostState.showSnackbar(
|
||||
message,
|
||||
actionLabel = "Dismiss",
|
||||
duration = SnackbarDuration.Indefinite
|
||||
)
|
||||
when (result) {
|
||||
SnackbarResult.ActionPerformed -> viewModel.resetErrorMessage()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AuthProviderView(viewModel: LoginViewModel) {
|
||||
val isGoogleAuthAvailable: Boolean =
|
||||
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(LocalContext.current) == 0
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (isGoogleAuthAvailable) {
|
||||
GoogleAuthButton(viewModel)
|
||||
}
|
||||
|
||||
AppleAuthButton(viewModel)
|
||||
|
||||
ClickableText(text = AnnotatedString(stringResource(R.string.welcome_screen_action_continue_with_email)),
|
||||
style = MaterialTheme.typography.titleMedium.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showEmailSignIn() })
|
||||
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.welcome_screen_action_self_hosting_options)),
|
||||
style = MaterialTheme.typography.titleMedium.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showSelfHostedSettings() },
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MoreInfoButton() {
|
||||
val context = LocalContext.current
|
||||
val intent = remember { Intent(Intent.ACTION_VIEW, Uri.parse("https://omnivore.app/about")) }
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(
|
||||
stringResource(id = R.string.learn_more),
|
||||
),
|
||||
style = MaterialTheme.typography.titleSmall.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = {
|
||||
context.startActivity(intent)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 6.dp)
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,213 @@
|
||||
package app.omnivore.omnivore.feature.onboarding
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.designsystem.theme.OmnivoreBrand
|
||||
import app.omnivore.omnivore.feature.onboarding.auth.AuthProviderScreen
|
||||
import app.omnivore.omnivore.feature.onboarding.auth.EmailSignInScreen
|
||||
import app.omnivore.omnivore.feature.onboarding.auth.EmailSignUpScreen
|
||||
import app.omnivore.omnivore.feature.onboarding.auth.SelfHostedScreen
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
import app.omnivore.omnivore.navigation.OmnivoreNavHost
|
||||
import app.omnivore.omnivore.navigation.Routes
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OnboardingScreen(
|
||||
navController: NavHostController,
|
||||
viewModel: LoginViewModel = hiltViewModel()
|
||||
) {
|
||||
|
||||
val activity = LocalContext.current as ComponentActivity
|
||||
val welcomeNavController = rememberNavController()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val currentRoute by welcomeNavController.currentBackStackEntryFlow.collectAsState(
|
||||
initial = welcomeNavController.currentBackStackEntry
|
||||
)
|
||||
|
||||
val errorMessage by viewModel.errorMessage.collectAsStateWithLifecycle()
|
||||
|
||||
OmnivoreTheme(darkTheme = false) {
|
||||
|
||||
LaunchedEffect(key1 = errorMessage) {
|
||||
errorMessage?.let { message ->
|
||||
val result = snackBarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = "Dismiss",
|
||||
duration = SnackbarDuration.Indefinite
|
||||
)
|
||||
when (result) {
|
||||
SnackbarResult.ActionPerformed -> viewModel.resetErrorMessage()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { },
|
||||
navigationIcon = {
|
||||
if (currentRoute?.destination?.route != Routes.AuthProvider.route) {
|
||||
IconButton(onClick = { welcomeNavController.popBackStack() }) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = OmnivoreBrand
|
||||
)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding(),
|
||||
snackbarHost = { SnackbarHost(hostState = snackBarHostState) },
|
||||
containerColor = OmnivoreBrand
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
item {
|
||||
WelcomeHeader()
|
||||
}
|
||||
item {
|
||||
OmnivoreNavHost(
|
||||
navController = welcomeNavController,
|
||||
startDestination = Routes.AuthProvider.route
|
||||
) {
|
||||
composable(Routes.AuthProvider.route) {
|
||||
AuthProviderScreen(
|
||||
navController = navController,
|
||||
welcomeNavController = welcomeNavController
|
||||
)
|
||||
}
|
||||
composable(Routes.EmailSignIn.route) {
|
||||
EmailSignInScreen(
|
||||
navController = navController,
|
||||
welcomeNavController = welcomeNavController
|
||||
)
|
||||
}
|
||||
composable(Routes.EmailSignUp.route) {
|
||||
EmailSignUpScreen()
|
||||
}
|
||||
composable(Routes.SelfHosting.route){
|
||||
SelfHostedScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the light status bar
|
||||
DisposableEffect(Unit) {
|
||||
val windowInsetsController = WindowInsetsControllerCompat(activity.window, activity.window.decorView)
|
||||
val originalAppearanceLightStatusBars = windowInsetsController.isAppearanceLightStatusBars
|
||||
val originalStatusBarColor = activity.window.statusBarColor
|
||||
|
||||
// Set light status bar
|
||||
windowInsetsController.isAppearanceLightStatusBars = true
|
||||
activity.window.statusBarColor = Color.Transparent.toArgb()
|
||||
|
||||
onDispose {
|
||||
// Restore original status bar settings
|
||||
windowInsetsController.isAppearanceLightStatusBars = originalAppearanceLightStatusBars
|
||||
activity.window.statusBarColor = originalStatusBarColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WelcomeHeader() {
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = 64.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_omnivore_name_logo),
|
||||
contentDescription = "Omnivore Icon with Name"
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.welcome_title),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.headlineLarge
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.welcome_subtitle),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
MoreInfoButton()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MoreInfoButton() {
|
||||
val context = LocalContext.current
|
||||
val intent = remember { Intent(Intent.ACTION_VIEW, Uri.parse("https://omnivore.app/about")) }
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(
|
||||
stringResource(id = R.string.learn_more),
|
||||
),
|
||||
style = MaterialTheme.typography.titleSmall.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = {
|
||||
context.startActivity(intent)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 6.dp)
|
||||
)
|
||||
}
|
||||
@ -1,11 +1,10 @@
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
package app.omnivore.omnivore.feature.onboarding
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
@ -43,8 +42,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import io.intercom.android.sdk.Intercom
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
@ -52,10 +53,6 @@ import kotlinx.coroutines.runBlocking
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
enum class RegistrationState {
|
||||
SocialLogin, EmailSignIn, EmailSignUp, PendingUser, SelfHosted
|
||||
}
|
||||
|
||||
data class PendingEmailUserCreds(
|
||||
val email: String, val password: String
|
||||
)
|
||||
@ -73,8 +70,8 @@ class LoginViewModel @Inject constructor(
|
||||
var isLoading by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var errorMessage by mutableStateOf<String?>(null)
|
||||
private set
|
||||
private val _errorMessage = MutableStateFlow<String?>(null)
|
||||
val errorMessage: StateFlow<String?> get() = _errorMessage.asStateFlow()
|
||||
|
||||
var hasValidUsername by mutableStateOf(false)
|
||||
private set
|
||||
@ -92,8 +89,6 @@ class LoginViewModel @Inject constructor(
|
||||
initialValue = true
|
||||
)
|
||||
|
||||
val registrationStateLiveData = MutableLiveData(RegistrationState.SocialLogin)
|
||||
|
||||
val followingTabActiveState: StateFlow<Boolean> = datastoreRepository.getBoolean(
|
||||
followingTabActive
|
||||
).stateIn(
|
||||
@ -124,29 +119,11 @@ class LoginViewModel @Inject constructor(
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun showSocialLogin() {
|
||||
resetState()
|
||||
registrationStateLiveData.value = RegistrationState.SocialLogin
|
||||
}
|
||||
|
||||
fun showEmailSignIn() {
|
||||
resetState()
|
||||
registrationStateLiveData.value = RegistrationState.EmailSignIn
|
||||
}
|
||||
|
||||
fun showEmailSignUp(pendingCreds: PendingEmailUserCreds? = null) {
|
||||
private fun showEmailSignUp(pendingCreds: PendingEmailUserCreds? = null) {
|
||||
resetState()
|
||||
pendingEmailUserCreds = pendingCreds
|
||||
registrationStateLiveData.value = RegistrationState.EmailSignUp
|
||||
}
|
||||
|
||||
fun showSelfHostedSettings() {
|
||||
resetState()
|
||||
registrationStateLiveData.value = RegistrationState.SelfHosted
|
||||
}
|
||||
|
||||
fun cancelNewUserSignUp() {
|
||||
@ -154,7 +131,7 @@ class LoginViewModel @Inject constructor(
|
||||
viewModelScope.launch {
|
||||
datastoreRepository.clearValue(omnivorePendingUserToken)
|
||||
}
|
||||
showSocialLogin()
|
||||
resetState()
|
||||
}
|
||||
|
||||
fun registerUser() {
|
||||
@ -166,10 +143,18 @@ class LoginViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetState() {
|
||||
private fun setErrorMessage(message: String) {
|
||||
_errorMessage.value = message
|
||||
}
|
||||
|
||||
fun resetErrorMessage() {
|
||||
_errorMessage.value = null
|
||||
}
|
||||
|
||||
fun resetState() {
|
||||
validateUsernameJob = null
|
||||
isLoading = false
|
||||
errorMessage = null
|
||||
resetErrorMessage()
|
||||
hasValidUsername = false
|
||||
usernameValidationErrorMessage = null
|
||||
pendingEmailUserCreds = null
|
||||
@ -240,7 +225,7 @@ class LoginViewModel @Inject constructor(
|
||||
RetrofitHelper.getInstance(networker).create(EmailLoginSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
resetErrorMessage()
|
||||
|
||||
val result = emailLogin.submitEmailLogin(
|
||||
EmailLoginCredentials(email = email, password = password)
|
||||
@ -260,9 +245,7 @@ class LoginViewModel @Inject constructor(
|
||||
if (result.body()?.authToken != null) {
|
||||
datastoreRepository.putString(omnivoreAuthToken, result.body()?.authToken!!)
|
||||
} else {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_error_msg
|
||||
)
|
||||
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_error_msg))
|
||||
}
|
||||
|
||||
if (result.body()?.authCookieString != null) {
|
||||
@ -284,7 +267,7 @@ class LoginViewModel @Inject constructor(
|
||||
RetrofitHelper.getInstance(networker).create(CreateEmailAccountSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
resetErrorMessage()
|
||||
|
||||
val params = EmailSignUpParams(
|
||||
email = email, password = password, name = name, username = username
|
||||
@ -295,9 +278,7 @@ class LoginViewModel @Inject constructor(
|
||||
isLoading = false
|
||||
|
||||
if (result.errorBody() != null) {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg
|
||||
)
|
||||
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_error_msg))
|
||||
} else {
|
||||
pendingEmailUserCreds = PendingEmailUserCreds(email, password)
|
||||
}
|
||||
@ -314,7 +295,7 @@ class LoginViewModel @Inject constructor(
|
||||
RetrofitHelper.getInstance(networker).create(CreateAccountSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
resetErrorMessage()
|
||||
|
||||
val pendingUserToken = getPendingAuthToken() ?: ""
|
||||
|
||||
@ -330,9 +311,7 @@ class LoginViewModel @Inject constructor(
|
||||
if (result.body()?.authToken != null) {
|
||||
datastoreRepository.putString(omnivoreAuthToken, result.body()?.authToken!!)
|
||||
} else {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_error_msg
|
||||
)
|
||||
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_error_msg))
|
||||
}
|
||||
|
||||
if (result.body()?.authCookieString != null) {
|
||||
@ -358,12 +337,8 @@ class LoginViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun resetErrorMessage() {
|
||||
errorMessage = null
|
||||
}
|
||||
|
||||
fun showGoogleErrorMessage() {
|
||||
errorMessage = resourceProvider.getString(R.string.login_view_model_google_auth_error_msg)
|
||||
setErrorMessage(resourceProvider.getString(R.string.login_view_model_google_auth_error_msg))
|
||||
}
|
||||
|
||||
fun handleGoogleAuthTask(task: Task<GoogleSignInAccount>) {
|
||||
@ -372,9 +347,7 @@ class LoginViewModel @Inject constructor(
|
||||
|
||||
// If token is missing then set the error message
|
||||
if (googleIdToken.isEmpty()) {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_missing_auth_token_error_msg
|
||||
)
|
||||
setErrorMessage(resourceProvider.getString(R.string.login_view_model_missing_auth_token_error_msg))
|
||||
return
|
||||
}
|
||||
|
||||
@ -390,7 +363,7 @@ class LoginViewModel @Inject constructor(
|
||||
RetrofitHelper.getInstance(networker).create(AuthProviderLoginSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
resetErrorMessage()
|
||||
|
||||
val result = login.submitAuthProviderLogin(params)
|
||||
|
||||
@ -413,15 +386,11 @@ class LoginViewModel @Inject constructor(
|
||||
|
||||
418 -> {
|
||||
// Show pending email state
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg
|
||||
)
|
||||
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_two_error_msg))
|
||||
}
|
||||
|
||||
else -> {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg
|
||||
)
|
||||
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_two_error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -430,7 +399,7 @@ class LoginViewModel @Inject constructor(
|
||||
|
||||
private suspend fun submitAuthProviderPayloadForPendingToken(params: SignInParams) {
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
resetErrorMessage()
|
||||
|
||||
val request = RetrofitHelper.getInstance(networker).create(PendingUserSubmit::class.java)
|
||||
val result = request.submitPendingUser(params)
|
||||
@ -441,11 +410,9 @@ class LoginViewModel @Inject constructor(
|
||||
datastoreRepository.putString(
|
||||
omnivorePendingUserToken, result.body()?.pendingUserToken!!
|
||||
)
|
||||
registrationStateLiveData.value = RegistrationState.PendingUser
|
||||
// TODO go to pending user
|
||||
} else {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg
|
||||
)
|
||||
setErrorMessage(resourceProvider.getString(R.string.login_view_model_something_went_wrong_two_error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package app.omnivore.omnivore.feature.onboarding.auth
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
|
||||
import app.omnivore.omnivore.feature.onboarding.auth.provider.AppleAuthButton
|
||||
import app.omnivore.omnivore.feature.onboarding.auth.provider.GoogleAuthButton
|
||||
import app.omnivore.omnivore.navigation.Routes
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
|
||||
@Composable
|
||||
fun AuthProviderScreen(
|
||||
navController: NavHostController,
|
||||
welcomeNavController: NavHostController,
|
||||
viewModel: LoginViewModel = hiltViewModel()
|
||||
) {
|
||||
val isGoogleAuthAvailable: Boolean =
|
||||
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(LocalContext.current) == 0
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 64.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.width(500.dp)
|
||||
) {
|
||||
if (isGoogleAuthAvailable) {
|
||||
GoogleAuthButton(viewModel)
|
||||
}
|
||||
|
||||
AppleAuthButton(viewModel)
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
welcomeNavController.navigate(Routes.EmailSignIn.route)
|
||||
viewModel.resetState()
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Text(text = stringResource(R.string.welcome_screen_action_continue_with_email), modifier = Modifier.padding(vertical = 6.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.resetState()
|
||||
welcomeNavController.navigate(Routes.SelfHosting.route)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 10.dp),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
){
|
||||
Text(
|
||||
text = stringResource(R.string.welcome_screen_action_self_hosting_options),
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
package app.omnivore.omnivore.feature.onboarding.auth
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
|
||||
|
||||
@Composable
|
||||
fun CreateUserProfileScreen(viewModel: LoginViewModel) {
|
||||
var name by rememberSaveable { mutableStateOf("") }
|
||||
var username by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.create_user_profile_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
UserProfileFields(name = name,
|
||||
username = username,
|
||||
usernameValidationErrorMessage = viewModel.usernameValidationErrorMessage,
|
||||
showUsernameAsAvailable = viewModel.hasValidUsername,
|
||||
onNameChange = { name = it },
|
||||
onUsernameChange = {
|
||||
username = it
|
||||
viewModel.validateUsername(it)
|
||||
},
|
||||
onSubmit = { viewModel.submitProfile(username = username, name = name) })
|
||||
|
||||
// TODO: add a activity indicator (maybe after a delay?)
|
||||
if (viewModel.isLoading) {
|
||||
Text(stringResource(R.string.create_user_profile_loading))
|
||||
}
|
||||
|
||||
ClickableText(text = AnnotatedString(stringResource(R.string.create_user_profile_action_cancel)),
|
||||
style = MaterialTheme.typography.titleMedium.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.cancelNewUserSignUp() })
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserProfileFields(
|
||||
name: String,
|
||||
username: String,
|
||||
usernameValidationErrorMessage: String?,
|
||||
showUsernameAsAvailable: Boolean,
|
||||
onNameChange: (String) -> Unit,
|
||||
onUsernameChange: (String) -> Unit,
|
||||
onSubmit: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(25.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
placeholder = { Text(stringResource(R.string.create_user_profile_field_placeholder_name)) },
|
||||
label = { Text(stringResource(R.string.create_user_profile_field_label_name)) },
|
||||
onValueChange = onNameChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(value = username,
|
||||
placeholder = { Text(stringResource(R.string.create_user_profile_field_placeholder_username)) },
|
||||
label = { Text(stringResource(R.string.create_user_profile_field_label_username)) },
|
||||
onValueChange = onUsernameChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
trailingIcon = {
|
||||
if (showUsernameAsAvailable) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.CheckCircle, contentDescription = null
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (usernameValidationErrorMessage != null) {
|
||||
Text(
|
||||
text = usernameValidationErrorMessage,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (name.isNotBlank() && username.isNotBlank()) {
|
||||
onSubmit()
|
||||
focusManager.clearFocus()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.create_user_profile_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D), containerColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.create_user_profile_action_submit),
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,212 @@
|
||||
package app.omnivore.omnivore.feature.onboarding.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.designsystem.component.DividerWithText
|
||||
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
import app.omnivore.omnivore.utils.AuthUtils.autofill
|
||||
import app.omnivore.omnivore.navigation.Routes
|
||||
import app.omnivore.omnivore.utils.FORGOT_PASSWORD_URL
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
fun EmailSignInScreen(
|
||||
navController: NavHostController,
|
||||
welcomeNavController: NavHostController
|
||||
) {
|
||||
OmnivoreTheme(darkTheme = false) {
|
||||
EmailSignInContent(navController, welcomeNavController)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EmailSignInContent(
|
||||
navController: NavHostController,
|
||||
welcomeNavController: NavHostController,
|
||||
viewModel: LoginViewModel = hiltViewModel()
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
var email by rememberSaveable { mutableStateOf("") }
|
||||
var password by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.padding(bottom = 64.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
LoginFields(
|
||||
email,
|
||||
password,
|
||||
onEmailChange = { email = it },
|
||||
onPasswordChange = { password = it },
|
||||
onLoginClick = { viewModel.login(email, password) },
|
||||
onCreateAccountClick = {
|
||||
welcomeNavController.navigate(Routes.EmailSignUp.route)
|
||||
viewModel.resetState()
|
||||
},
|
||||
isLoading = viewModel.isLoading
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LoginFields(
|
||||
email: String,
|
||||
password: String,
|
||||
onEmailChange: (String) -> Unit,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
onLoginClick: () -> Unit,
|
||||
onCreateAccountClick: () -> Unit,
|
||||
isLoading: Boolean
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(modifier = Modifier
|
||||
.autofill(autofillTypes = listOf(
|
||||
AutofillType.EmailAddress,
|
||||
), onFill = { onEmailChange(it) })
|
||||
.fillMaxWidth(),
|
||||
value = email,
|
||||
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_email)) },
|
||||
label = { Text(stringResource(R.string.email_login_field_label_email)) },
|
||||
onValueChange = onEmailChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Email,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
OutlinedTextField(modifier = Modifier
|
||||
.autofill(autofillTypes = listOf(
|
||||
AutofillType.Password,
|
||||
), onFill = { onPasswordChange(it) })
|
||||
.fillMaxWidth(),
|
||||
value = password,
|
||||
placeholder = { Text(stringResource(R.string.email_login_field_placeholder_password)) },
|
||||
label = { Text(stringResource(R.string.email_login_field_label_password)) },
|
||||
onValueChange = onPasswordChange,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Password,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val uri = FORGOT_PASSWORD_URL
|
||||
uriHandler.openUri(uri)
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(R.string.forgot_password))
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = email.isNotBlank() && password.isNotBlank(),
|
||||
onClick = {
|
||||
if (email.isNotBlank() && password.isNotBlank()) {
|
||||
onLoginClick()
|
||||
focusManager.clearFocus()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.email_login_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D), containerColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.email_login_action_login).uppercase()
|
||||
)
|
||||
if (isLoading) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(16.dp)
|
||||
.width(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DividerWithText(text = "or")
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { onCreateAccountClick() },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D), containerColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Create Account".uppercase()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,259 @@
|
||||
package app.omnivore.omnivore.feature.onboarding.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
|
||||
import app.omnivore.omnivore.utils.AuthUtils.autofill
|
||||
|
||||
@Composable
|
||||
fun EmailSignUpScreen(
|
||||
viewModel: LoginViewModel = hiltViewModel()
|
||||
) {
|
||||
if (viewModel.pendingEmailUserCreds != null) {
|
||||
val email = viewModel.pendingEmailUserCreds?.email ?: ""
|
||||
val password = viewModel.pendingEmailUserCreds?.password ?: ""
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.email_signup_verification_message, email),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.login(email, password)
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D), containerColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.email_signup_check_status),
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
)
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.email_signup_action_use_different_email)),
|
||||
style = MaterialTheme.typography.titleMedium.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.resetState() }
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EmailSignUpForm(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
fun EmailSignUpForm(viewModel: LoginViewModel) {
|
||||
var email by rememberSaveable { mutableStateOf("") }
|
||||
var password by rememberSaveable { mutableStateOf("") }
|
||||
var name by rememberSaveable { mutableStateOf("") }
|
||||
var username by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.padding(bottom = 64.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
EmailSignUpFields(
|
||||
email = email,
|
||||
password = password,
|
||||
name = name,
|
||||
username = username,
|
||||
usernameValidationErrorMessage = viewModel.usernameValidationErrorMessage,
|
||||
showUsernameAsAvailable = viewModel.hasValidUsername,
|
||||
onEmailChange = { email = it },
|
||||
onPasswordChange = { password = it },
|
||||
onNameChange = { name = it },
|
||||
onUsernameChange = {
|
||||
username = it
|
||||
viewModel.validateUsername(it)
|
||||
},
|
||||
onSubmit = {
|
||||
viewModel.submitEmailSignUp(
|
||||
email = email, password = password, username = username, name = name
|
||||
)
|
||||
},
|
||||
isLoading = viewModel.isLoading
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun EmailSignUpFields(
|
||||
email: String,
|
||||
password: String,
|
||||
name: String,
|
||||
username: String,
|
||||
usernameValidationErrorMessage: String?,
|
||||
showUsernameAsAvailable: Boolean,
|
||||
onEmailChange: (String) -> Unit,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
onNameChange: (String) -> Unit,
|
||||
onUsernameChange: (String) -> Unit,
|
||||
onSubmit: () -> Unit,
|
||||
isLoading: Boolean
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.autofill(
|
||||
autofillTypes = listOf(AutofillType.EmailAddress),
|
||||
onFill = { onEmailChange(it) }
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
value = email,
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_email)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_email)) },
|
||||
onValueChange = onEmailChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
OutlinedTextField(modifier = Modifier.autofill(autofillTypes = listOf(
|
||||
AutofillType.Password,
|
||||
), onFill = { onPasswordChange(it) }).fillMaxWidth(),
|
||||
value = password,
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_password)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_password)) },
|
||||
onValueChange = onPasswordChange,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = name,
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_name)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_name)) },
|
||||
onValueChange = onNameChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 32.dp),
|
||||
value = username,
|
||||
placeholder = { Text(stringResource(R.string.email_signup_field_placeholder_username)) },
|
||||
label = { Text(stringResource(R.string.email_signup_field_label_username)) },
|
||||
onValueChange = onUsernameChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
isError = usernameValidationErrorMessage != null,
|
||||
trailingIcon = {
|
||||
if (showUsernameAsAvailable) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.CheckCircle, contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
supportingText = {
|
||||
if (usernameValidationErrorMessage != null) {
|
||||
Text(
|
||||
text = usernameValidationErrorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Left
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
if (email.isNotBlank() && password.isNotBlank() && username.isNotBlank() && name.isNotBlank()) {
|
||||
onSubmit()
|
||||
focusManager.clearFocus()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.email_signup_error_msg),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D), containerColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.email_signup_action_sign_up).uppercase()
|
||||
)
|
||||
if (isLoading) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(16.dp)
|
||||
.width(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,31 @@
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
package app.omnivore.omnivore.feature.onboarding.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@ -24,20 +39,27 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.designsystem.component.DividerWithText
|
||||
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
|
||||
import app.omnivore.omnivore.utils.SELF_HOSTING_URL
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
fun SelfHostedView(viewModel: LoginViewModel) {
|
||||
fun SelfHostedScreen(
|
||||
viewModel: LoginViewModel = hiltViewModel()
|
||||
) {
|
||||
var apiServer by rememberSaveable { mutableStateOf("") }
|
||||
var webServer by rememberSaveable { mutableStateOf("") }
|
||||
val context = LocalContext.current
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.padding(bottom = 64.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
@ -48,51 +70,25 @@ fun SelfHostedView(viewModel: LoginViewModel) {
|
||||
onWebServerChange = { webServer = it },
|
||||
onSaveClick = {
|
||||
viewModel.setSelfHostingDetails(context, apiServer, webServer)
|
||||
}
|
||||
},
|
||||
onResetClick = { viewModel.resetSelfHostingDetails(context) },
|
||||
isLoading = viewModel.isLoading
|
||||
)
|
||||
|
||||
// TODO: add a activity indicator (maybe after a delay?)
|
||||
if (viewModel.isLoading) {
|
||||
Text(stringResource(R.string.self_hosted_view_loading))
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_reset)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.resetSelfHostingDetails(context) },
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_back)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showSocialLogin() },
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
// Text("Omnivore is a free and open-source software project and allows self hosting. \n\n" +
|
||||
// "If you have chosen to deploy your own server instance, fill in the above fields to " +
|
||||
// "your private self-hosted instance.\n\n"
|
||||
// )
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_learn_more)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = {
|
||||
val uri = Uri.parse("https://docs.omnivore.app/self-hosting/self-hosting.html")
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, uri)
|
||||
ContextCompat.startActivity(context, browserIntent, null)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
}
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.self_hosted_view_action_learn_more)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = {
|
||||
val uri = Uri.parse(SELF_HOSTING_URL)
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, uri)
|
||||
ContextCompat.startActivity(context, browserIntent, null)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
@ -105,19 +101,19 @@ fun SelfHostedFields(
|
||||
webServer: String,
|
||||
onAPIServerChange: (String) -> Unit,
|
||||
onWebServerChange: (String) -> Unit,
|
||||
onSaveClick: () -> Unit
|
||||
onSaveClick: () -> Unit,
|
||||
onResetClick: () -> Unit,
|
||||
isLoading: Boolean
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(25.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = apiServer,
|
||||
placeholder = { Text(text = "https://api-prod.omnivore.app/") },
|
||||
label = { Text(stringResource(R.string.self_hosted_view_field_api_url_label)) },
|
||||
@ -130,6 +126,7 @@ fun SelfHostedFields(
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 32.dp),
|
||||
value = webServer,
|
||||
placeholder = { Text(text = "https://omnivore.app/") },
|
||||
label = { Text(stringResource(R.string.self_hosted_view_field_web_url_label)) },
|
||||
@ -141,7 +138,9 @@ fun SelfHostedFields(
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
Button(onClick = {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
if (apiServer.isNotBlank() && webServer.isNotBlank()) {
|
||||
onSaveClick()
|
||||
focusManager.clearFocus()
|
||||
@ -158,8 +157,28 @@ fun SelfHostedFields(
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.self_hosted_view_action_save),
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
text = stringResource(R.string.self_hosted_view_action_save).uppercase(),
|
||||
)
|
||||
if (isLoading) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(16.dp)
|
||||
.width(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DividerWithText(text = "or")
|
||||
|
||||
OutlinedButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { onResetClick() }
|
||||
) {
|
||||
Text(
|
||||
text = "Reset".uppercase()
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
package app.omnivore.omnivore.feature.onboarding.auth.provider
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
|
||||
import app.omnivore.omnivore.utils.AppleConstants
|
||||
import java.net.URLEncoder
|
||||
|
||||
@Composable
|
||||
fun AppleAuthButton(viewModel: LoginViewModel) {
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
showDialog.value = true
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_logo_apple),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.padding(end = 10.dp)
|
||||
)
|
||||
Text(text = stringResource(R.string.apple_auth_text), modifier = Modifier.padding(vertical = 6.dp))
|
||||
if (viewModel.isLoading) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(16.dp)
|
||||
.width(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog.value) {
|
||||
AppleAuthDialog(onDismiss = { token ->
|
||||
if (token != null) {
|
||||
viewModel.handleAppleToken(token)
|
||||
}
|
||||
showDialog.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppleAuthDialog(onDismiss: (String?) -> Unit) {
|
||||
Dialog(onDismissRequest = { onDismiss(null) }) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp), color = Color.White
|
||||
) {
|
||||
AppleAuthWebView(onDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
fun AppleAuthWebView(onDismiss: (String?) -> Unit) {
|
||||
val url =
|
||||
AppleConstants.authUrl + "?client_id=" + AppleConstants.clientId + "&redirect_uri=" + URLEncoder.encode(
|
||||
AppleConstants.redirectURI,
|
||||
"utf8"
|
||||
) + "&response_type=code%20id_token" + "&scope=" + AppleConstants.scope + "&response_mode=form_post" + "&state=android:login"
|
||||
|
||||
// Adding a WebView inside AndroidView
|
||||
// with layout as full screen
|
||||
AndroidView(factory = {
|
||||
WebView(it).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?, request: WebResourceRequest?
|
||||
): Boolean {
|
||||
if (request?.url.toString().contains("android-apple-token")) {
|
||||
val uri = Uri.parse(request!!.url.toString())
|
||||
val token = uri.getQueryParameter("token")
|
||||
|
||||
onDismiss(token)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
settings.javaScriptEnabled = true
|
||||
loadUrl(url)
|
||||
}
|
||||
}, update = {
|
||||
it.loadUrl(url)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
package app.omnivore.omnivore.feature.onboarding.auth.provider
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
||||
import com.google.android.gms.tasks.Task
|
||||
|
||||
@Composable
|
||||
fun GoogleAuthButton(viewModel: LoginViewModel) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
||||
.requestIdToken(BuildConfig.OMNIVORE_GAUTH_SERVER_CLIENT_ID).requestEmail().build()
|
||||
|
||||
val startForResult =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val intent = result.data
|
||||
if (result.data != null) {
|
||||
val task: Task<GoogleSignInAccount> =
|
||||
GoogleSignIn.getSignedInAccountFromIntent(intent)
|
||||
viewModel.handleGoogleAuthTask(task)
|
||||
}
|
||||
} else {
|
||||
viewModel.showGoogleErrorMessage()
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
val googleSignIn = GoogleSignIn.getClient(context, signInOptions)
|
||||
|
||||
googleSignIn.silentSignIn().addOnCompleteListener { task ->
|
||||
if (task.isSuccessful) {
|
||||
viewModel.handleGoogleAuthTask(task)
|
||||
} else {
|
||||
startForResult.launch(googleSignIn.signInIntent)
|
||||
}
|
||||
}.addOnFailureListener {
|
||||
startForResult.launch(googleSignIn.signInIntent)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_logo_google),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.padding(end = 10.dp)
|
||||
)
|
||||
Text(text = stringResource(R.string.google_auth_text), modifier = Modifier.padding(vertical = 6.dp))
|
||||
if (viewModel.isLoading) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(16.dp)
|
||||
.width(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,7 +18,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.designsystem.component.TextPreferenceWidget
|
||||
import app.omnivore.omnivore.feature.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
|
||||
import app.omnivore.omnivore.navigation.Routes
|
||||
|
||||
internal const val RELEASE_URL = "https://github.com/omnivore-app/omnivore/releases"
|
||||
|
||||
@ -1,68 +1,78 @@
|
||||
package app.omnivore.omnivore.feature.reader
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import com.google.gson.Gson
|
||||
|
||||
enum class WebFont(val displayText: String, val rawValue: String) {
|
||||
INTER("Inter", "Inter"),
|
||||
SYSTEM("System Default", "system-ui"),
|
||||
OPEN_DYSLEXIC("Open Dyslexic", "OpenDyslexic"),
|
||||
MERRIWEATHER("Merriweather", "Merriweather"),
|
||||
LORA("Lora", "Lora"),
|
||||
OPEN_SANS("Open Sans", "Open Sans"),
|
||||
ROBOTO("Roboto", "Roboto"),
|
||||
CRIMSON_TEXT("Crimson Text", "Crimson Text"),
|
||||
SOURCE_SERIF_PRO("Source Serif Pro", "Source Serif Pro"),
|
||||
NEWSREADER("Newsreader", "Newsreader"),
|
||||
LEXEND("Lexend", "Lexend"),
|
||||
LXGWWENKAI("LXGW WenKai", "LXGWWenKai"),
|
||||
ATKINSON_HYPERLEGIBLE("Atkinson Hyperlegible", "AtkinsonHyperlegible"),
|
||||
SOURCE_SANS_PRO("Source Sans Pro", "SourceSansPro"),
|
||||
IBM_PLEX_SANS("IBM Plex Sans", "IBMPlexSans"),
|
||||
LITERATA("Literata", "Literata"),
|
||||
FRAUNCES("Fraunces", "Fraunces"),
|
||||
INTER("Inter", "Inter"), SYSTEM("System Default", "system-ui"), OPEN_DYSLEXIC(
|
||||
"Open Dyslexic",
|
||||
"OpenDyslexic"
|
||||
),
|
||||
MERRIWEATHER("Merriweather", "Merriweather"), LORA("Lora", "Lora"), OPEN_SANS(
|
||||
"Open Sans",
|
||||
"Open Sans"
|
||||
),
|
||||
ROBOTO("Roboto", "Roboto"), CRIMSON_TEXT(
|
||||
"Crimson Text",
|
||||
"Crimson Text"
|
||||
),
|
||||
SOURCE_SERIF_PRO("Source Serif Pro", "Source Serif Pro"), NEWSREADER(
|
||||
"Newsreader",
|
||||
"Newsreader"
|
||||
),
|
||||
LEXEND("Lexend", "Lexend"), LXGWWENKAI(
|
||||
"LXGW WenKai",
|
||||
"LXGWWenKai"
|
||||
),
|
||||
ATKINSON_HYPERLEGIBLE(
|
||||
"Atkinson Hyperlegible",
|
||||
"AtkinsonHyperlegible"
|
||||
),
|
||||
SOURCE_SANS_PRO("Source Sans Pro", "SourceSansPro"), IBM_PLEX_SANS(
|
||||
"IBM Plex Sans",
|
||||
"IBMPlexSans"
|
||||
),
|
||||
LITERATA("Literata", "Literata"), FRAUNCES("Fraunces", "Fraunces"),
|
||||
}
|
||||
|
||||
enum class ArticleContentStatus(val rawValue: String) {
|
||||
FAILED("FAILED"),
|
||||
PROCESSING("PROCESSING"),
|
||||
SUCCEEDED("SUCCEEDED"),
|
||||
UNKNOWN("UNKNOWN")
|
||||
FAILED("FAILED"), PROCESSING("PROCESSING"), SUCCEEDED("SUCCEEDED"), UNKNOWN("UNKNOWN")
|
||||
}
|
||||
|
||||
data class ArticleContent(
|
||||
val title: String,
|
||||
val htmlContent: String,
|
||||
val highlights: List<Highlight>,
|
||||
val contentStatus: String, // ArticleContentStatus,
|
||||
val labelsJSONString: String
|
||||
val title: String,
|
||||
val htmlContent: String,
|
||||
val highlights: List<Highlight>,
|
||||
val contentStatus: String, // ArticleContentStatus,
|
||||
val labelsJSONString: String
|
||||
) {
|
||||
fun highlightsJSONString(): String {
|
||||
return Gson().toJson(highlights)
|
||||
}
|
||||
fun highlightsJSONString(): String {
|
||||
return Gson().toJson(highlights)
|
||||
}
|
||||
}
|
||||
|
||||
data class WebReaderContent(
|
||||
val preferences: WebPreferences,
|
||||
val item: SavedItem,
|
||||
val articleContent: ArticleContent,
|
||||
val preferences: WebPreferences,
|
||||
val item: SavedItem,
|
||||
val articleContent: ArticleContent,
|
||||
) {
|
||||
fun styledContent(): String {
|
||||
val savedAt = "\"${item.savedAt}\""
|
||||
val createdAt = "\"${item.createdAt}\""
|
||||
val publishedAt = if (item.publishDate != null) "\"${item.publishDate}\"" else "undefined"
|
||||
fun styledContent(): String {
|
||||
val savedAt = "\"${item.savedAt}\""
|
||||
val createdAt = "\"${item.createdAt}\""
|
||||
val publishedAt = if (item.publishDate != null) "\"${item.publishDate}\"" else "undefined"
|
||||
|
||||
|
||||
val textFontSize = preferences.textFontSize
|
||||
val highlightCssFilePath = "highlight${if (preferences.themeKey == "Dark" || preferences.themeKey == "Black") "-dark" else ""}.css"
|
||||
val textFontSize = preferences.textFontSize
|
||||
val highlightCssFilePath =
|
||||
"highlight${if (preferences.themeKey == "Dark" || preferences.themeKey == "Black") "-dark" else ""}.css"
|
||||
|
||||
Log.d("theme", "current theme is: ${preferences.themeKey}")
|
||||
Log.d("theme", "current theme is: ${preferences.themeKey}")
|
||||
|
||||
Log.d("sync", "HIGHLIGHTS JSON: ${articleContent.highlightsJSONString()}")
|
||||
Log.d("sync", "HIGHLIGHTS JSON: ${articleContent.highlightsJSONString()}")
|
||||
|
||||
return """
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@ -118,5 +128,5 @@ data class WebReaderContent(
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
@ -34,15 +33,13 @@ import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import app.omnivore.omnivore.core.designsystem.motion.materialSharedAxisXIn
|
||||
import app.omnivore.omnivore.core.designsystem.motion.materialSharedAxisXOut
|
||||
import app.omnivore.omnivore.feature.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.feature.auth.WelcomeScreen
|
||||
import app.omnivore.omnivore.core.designsystem.theme.OmnivoreBrand
|
||||
import app.omnivore.omnivore.feature.onboarding.LoginViewModel
|
||||
import app.omnivore.omnivore.feature.onboarding.OnboardingScreen
|
||||
import app.omnivore.omnivore.feature.following.FollowingScreen
|
||||
import app.omnivore.omnivore.feature.library.LibraryView
|
||||
import app.omnivore.omnivore.feature.library.SearchView
|
||||
@ -51,6 +48,7 @@ import app.omnivore.omnivore.feature.profile.about.AboutScreen
|
||||
import app.omnivore.omnivore.feature.profile.account.AccountScreen
|
||||
import app.omnivore.omnivore.feature.profile.filters.FiltersScreen
|
||||
import app.omnivore.omnivore.feature.web.WebViewScreen
|
||||
import app.omnivore.omnivore.navigation.OmnivoreNavHost
|
||||
import app.omnivore.omnivore.navigation.Routes
|
||||
import app.omnivore.omnivore.navigation.TopLevelDestination
|
||||
|
||||
@ -81,7 +79,7 @@ fun RootView(
|
||||
}
|
||||
}) { padding ->
|
||||
Box(
|
||||
modifier = if (!hasAuthToken) Modifier.background(Color(0xFFFCEBA8)) else Modifier
|
||||
modifier = if (!hasAuthToken) Modifier.background(OmnivoreBrand) else Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
@ -95,8 +93,7 @@ fun RootView(
|
||||
PrimaryNavigator(
|
||||
navController = navController,
|
||||
snackbarHostState = snackbarHostState,
|
||||
startDestination = startDestination,
|
||||
loginViewModel = loginViewModel
|
||||
startDestination = startDestination
|
||||
)
|
||||
LaunchedEffect(hasAuthToken) {
|
||||
if (hasAuthToken) {
|
||||
@ -107,33 +104,32 @@ fun RootView(
|
||||
}
|
||||
}
|
||||
|
||||
private const val INITIAL_OFFSET_FACTOR = 0.10f
|
||||
|
||||
|
||||
@Composable
|
||||
fun PrimaryNavigator(
|
||||
navController: NavHostController,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
startDestination: String,
|
||||
loginViewModel: LoginViewModel
|
||||
startDestination: String
|
||||
) {
|
||||
|
||||
NavHost(navController = navController,
|
||||
startDestination = startDestination,
|
||||
enterTransition = { materialSharedAxisXIn(initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
||||
exitTransition = { materialSharedAxisXOut(targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
||||
popEnterTransition = { materialSharedAxisXIn(initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
||||
popExitTransition = { materialSharedAxisXOut(targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) }) {
|
||||
OmnivoreNavHost(
|
||||
navController = navController,
|
||||
startDestination = startDestination
|
||||
) {
|
||||
|
||||
composable(Routes.Welcome.route) {
|
||||
WelcomeScreen(viewModel = loginViewModel)
|
||||
OnboardingScreen(navController = navController)
|
||||
}
|
||||
|
||||
navigation(startDestination = Routes.Inbox.route,
|
||||
navigation(
|
||||
startDestination = Routes.Inbox.route,
|
||||
route = Routes.Home.route,
|
||||
enterTransition = { EnterTransition.None },
|
||||
exitTransition = { ExitTransition.None },
|
||||
popEnterTransition = { EnterTransition.None },
|
||||
popExitTransition = { ExitTransition.None }) {
|
||||
popExitTransition = { ExitTransition.None }
|
||||
) {
|
||||
|
||||
composable(Routes.Inbox.route) {
|
||||
LibraryView(navController = navController)
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
package app.omnivore.omnivore.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import app.omnivore.omnivore.core.designsystem.motion.materialSharedAxisXIn
|
||||
import app.omnivore.omnivore.core.designsystem.motion.materialSharedAxisXOut
|
||||
|
||||
private const val INITIAL_OFFSET_FACTOR = 0.10f
|
||||
|
||||
@Composable
|
||||
fun OmnivoreNavHost(
|
||||
navController: NavHostController,
|
||||
startDestination: String,
|
||||
builder: NavGraphBuilder.() -> Unit
|
||||
) {
|
||||
return NavHost(
|
||||
navController = navController,
|
||||
startDestination = startDestination,
|
||||
enterTransition = { materialSharedAxisXIn(initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
||||
exitTransition = { materialSharedAxisXOut(targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
||||
popEnterTransition = { materialSharedAxisXIn(initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
||||
popExitTransition = { materialSharedAxisXOut(targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) }
|
||||
) {
|
||||
builder()
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,10 @@ package app.omnivore.omnivore.navigation
|
||||
sealed class Routes(val route: String) {
|
||||
data object Home : Routes("Home")
|
||||
data object Welcome : Routes("Welcome")
|
||||
data object EmailSignIn : Routes("EmailSignIn")
|
||||
data object EmailSignUp : Routes("EmailSignUp")
|
||||
data object SelfHosting : Routes("SelfHosting")
|
||||
data object AuthProvider : Routes("AuthProvider")
|
||||
data object Following : Routes("Following")
|
||||
data object Inbox : Routes("Inbox")
|
||||
data object Settings : Routes("Settings")
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.feature.auth
|
||||
package app.omnivore.omnivore.utils
|
||||
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -13,3 +13,6 @@ object AppleConstants {
|
||||
const val scope = "name%20email"
|
||||
const val authUrl = "https://appleid.apple.com/auth/authorize"
|
||||
}
|
||||
|
||||
const val FORGOT_PASSWORD_URL = "${BuildConfig.OMNIVORE_WEB_URL}/auth/forgot-password"
|
||||
const val SELF_HOSTING_URL = "https://docs.omnivore.app/self-hosting/self-hosting.html"
|
||||
|
||||
@ -268,4 +268,5 @@
|
||||
<string name="edit_info_sheet_action_cancel">Cancel</string>
|
||||
<string name="edit_info_sheet_error">Error while editing article!</string>
|
||||
<string name="edit_info_sheet_success">Article info successfully updated!</string>
|
||||
<string name="forgot_password">Forgot Password?</string>
|
||||
</resources>
|
||||
|
||||
@ -15,6 +15,8 @@ buildscript {
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
alias(libs.plugins.compose.compiler) apply false
|
||||
alias(libs.plugins.org.jetbrains.kotlin.android) apply false
|
||||
}
|
||||
|
||||
task<Delete>("clean") {
|
||||
|
||||
@ -2,14 +2,13 @@
|
||||
accompanistFlowLayout = "0.34.0"
|
||||
androidGradlePlugin = "8.3.2"
|
||||
androidxActivity = "1.9.0"
|
||||
androidxAppCompat = "1.6.1"
|
||||
androidxComposeBom = "2024.04.01"
|
||||
androidxComposeCompiler = "1.5.9"
|
||||
androidxCore = "1.13.0"
|
||||
androidxDataStore = "1.1.0"
|
||||
androidxAppCompat = "1.7.0"
|
||||
androidxComposeBom = "2024.06.00"
|
||||
androidxCore = "1.13.1"
|
||||
androidxDataStore = "1.1.1"
|
||||
androidxEspresso = "3.5.1"
|
||||
androidxHiltNavigationCompose = "1.2.0"
|
||||
androidxLifecycle = "2.7.0"
|
||||
androidxLifecycle = "2.8.2"
|
||||
androidxNavigation = "2.7.7"
|
||||
androidxSecurity = "1.0.0"
|
||||
androidxTestExt = "1.1.5"
|
||||
@ -19,14 +18,14 @@ coil = "2.6.0"
|
||||
composeMarkdown = "0.3.3"
|
||||
coreSplashscreen = "1.0.1"
|
||||
gson = "2.10.1"
|
||||
hilt = "2.51"
|
||||
hilt = "2.51.1"
|
||||
intercom = "15.8.2"
|
||||
junit4 = "4.13.2"
|
||||
kotlin = "1.9.22"
|
||||
ksp = "1.9.22-1.0.18"
|
||||
kotlin = "2.0.0"
|
||||
ksp = "2.0.0-1.0.21"
|
||||
kotlinxCoroutines = "1.8.0"
|
||||
playServices = "18.4.0"
|
||||
playServicesAuth = "21.1.0"
|
||||
playServices = "18.5.0"
|
||||
playServicesAuth = "21.2.0"
|
||||
posthog = "2.0.3"
|
||||
pspdfkit = "8.9.1"
|
||||
retrofit = "2.11.0"
|
||||
@ -88,6 +87,8 @@ hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltWo
|
||||
hilt-work-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltWork"}
|
||||
|
||||
[plugins]
|
||||
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
apollo = { id = "com.apollographql.apollo3", version.ref = "apollo" }
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
Reference in New Issue
Block a user