Merge pull request #2844 from omnivore-app/feat/android-self-hosting-options
Add self hosted settings to Android app
This commit is contained in:
@ -6,6 +6,8 @@ object Constants {
|
||||
}
|
||||
|
||||
object DatastoreKeys {
|
||||
const val omnivoreSelfHostedAPIServer = "omnivoreSelfHostedAPIServer"
|
||||
const val omnivoreSelfHostedWebServer = "omnivoreSelfHostedWebServer"
|
||||
const val omnivoreAuthToken = "omnivoreAuthToken"
|
||||
const val omnivoreAuthCookieString = "omnivoreAuthCookieString"
|
||||
const val omnivorePendingUserToken = "omnivorePendingUserToken"
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package app.omnivore.omnivore
|
||||
|
||||
import app.omnivore.omnivore.networking.Networker
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
@ -81,8 +82,9 @@ interface CreateEmailAccountSubmit {
|
||||
}
|
||||
|
||||
object RetrofitHelper {
|
||||
fun getInstance(): Retrofit {
|
||||
return Retrofit.Builder().baseUrl(Constants.apiURL)
|
||||
suspend fun getInstance(networker: Networker): Retrofit {
|
||||
val baseUrl = networker.baseUrl()
|
||||
return Retrofit.Builder().baseUrl(baseUrl)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
||||
@ -9,15 +9,17 @@ import javax.inject.Inject
|
||||
class Networker @Inject constructor(
|
||||
private val datastoreRepo: DatastoreRepository
|
||||
) {
|
||||
private val serverUrl = "${Constants.apiURL}/api/graphql"
|
||||
suspend fun baseUrl() = datastoreRepo.getString(DatastoreKeys.omnivoreSelfHostedAPIServer) ?: Constants.apiURL
|
||||
|
||||
suspend fun serverUrl() = "${baseUrl()}/api/graphql"
|
||||
private suspend fun authToken() = datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken) ?: ""
|
||||
|
||||
suspend fun publicApolloClient() = ApolloClient.Builder()
|
||||
.serverUrl(serverUrl)
|
||||
.serverUrl(serverUrl())
|
||||
.build()
|
||||
|
||||
suspend fun authenticatedApolloClient() = ApolloClient.Builder()
|
||||
.serverUrl(serverUrl)
|
||||
.serverUrl(serverUrl())
|
||||
.addHttpHeader("Authorization", value = authToken())
|
||||
.build()
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package app.omnivore.omnivore.ui.auth
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
@ -24,7 +26,8 @@ enum class RegistrationState {
|
||||
SocialLogin,
|
||||
EmailSignIn,
|
||||
EmailSignUp,
|
||||
PendingUser
|
||||
PendingUser,
|
||||
SelfHosted
|
||||
}
|
||||
|
||||
data class PendingEmailUserCreds(
|
||||
@ -67,6 +70,31 @@ class LoginViewModel @Inject constructor(
|
||||
datastoreRepo.getString(DatastoreKeys.omnivoreAuthCookieString)
|
||||
}
|
||||
|
||||
fun setSelfHostingDetails(context: Context, apiServer: String, webServer: String) {
|
||||
viewModelScope.launch {
|
||||
datastoreRepo.putString(DatastoreKeys.omnivoreSelfHostedAPIServer, apiServer)
|
||||
datastoreRepo.putString(DatastoreKeys.omnivoreSelfHostedWebServer, webServer)
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Self-hosting settings updated.",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun resetSelfHostingDetails(context: Context) {
|
||||
viewModelScope.launch {
|
||||
datastoreRepo.clearValue(DatastoreKeys.omnivoreSelfHostedAPIServer)
|
||||
datastoreRepo.clearValue(DatastoreKeys.omnivoreSelfHostedWebServer)
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Self-hosting settings reset.",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
fun showSocialLogin() {
|
||||
resetState()
|
||||
registrationStateLiveData.value = RegistrationState.SocialLogin
|
||||
@ -83,6 +111,11 @@ class LoginViewModel @Inject constructor(
|
||||
registrationStateLiveData.value = RegistrationState.EmailSignUp
|
||||
}
|
||||
|
||||
fun showSelfHostedSettings(pendingCreds: PendingEmailUserCreds? = null) {
|
||||
resetState()
|
||||
registrationStateLiveData.value = RegistrationState.SelfHosted
|
||||
}
|
||||
|
||||
fun cancelNewUserSignUp() {
|
||||
resetState()
|
||||
viewModelScope.launch {
|
||||
@ -162,9 +195,10 @@ class LoginViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun login(email: String, password: String) {
|
||||
val emailLogin = RetrofitHelper.getInstance().create(EmailLoginSubmit::class.java)
|
||||
|
||||
viewModelScope.launch {
|
||||
val emailLogin = RetrofitHelper.getInstance(networker).create(EmailLoginSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
@ -200,7 +234,7 @@ class LoginViewModel @Inject constructor(
|
||||
name: String,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val request = RetrofitHelper.getInstance().create(CreateEmailAccountSubmit::class.java)
|
||||
val request = RetrofitHelper.getInstance(networker).create(CreateEmailAccountSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
@ -230,7 +264,7 @@ class LoginViewModel @Inject constructor(
|
||||
|
||||
fun submitProfile(username: String, name: String) {
|
||||
viewModelScope.launch {
|
||||
val request = RetrofitHelper.getInstance().create(CreateAccountSubmit::class.java)
|
||||
val request = RetrofitHelper.getInstance(networker).create(CreateAccountSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
@ -300,9 +334,10 @@ class LoginViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun submitAuthProviderPayload(params: SignInParams) {
|
||||
val login = RetrofitHelper.getInstance().create(AuthProviderLoginSubmit::class.java)
|
||||
|
||||
viewModelScope.launch {
|
||||
val login = RetrofitHelper.getInstance(networker).create(AuthProviderLoginSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
@ -340,7 +375,7 @@ class LoginViewModel @Inject constructor(
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
val request = RetrofitHelper.getInstance().create(PendingUserSubmit::class.java)
|
||||
val request = RetrofitHelper.getInstance(networker).create(PendingUserSubmit::class.java)
|
||||
val result = request.submitPendingUser(params)
|
||||
|
||||
isLoading = false
|
||||
|
||||
@ -0,0 +1,176 @@
|
||||
package app.omnivore.omnivore.ui.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
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.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
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 androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.DatastoreKeys
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@Composable
|
||||
fun SelfHostedView(viewModel: LoginViewModel) {
|
||||
var apiServer by rememberSaveable { mutableStateOf("") }
|
||||
var webServer by rememberSaveable { mutableStateOf("") }
|
||||
var context = LocalContext.current
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
SelfHostedFields(
|
||||
apiServer,
|
||||
webServer,
|
||||
onAPIServerChange = { apiServer = it },
|
||||
onWebServerChange = { webServer = it },
|
||||
onSaveClick = {
|
||||
viewModel.setSelfHostingDetails(context, apiServer, webServer)
|
||||
}
|
||||
)
|
||||
|
||||
// TODO: add a activity indicator (maybe after a delay?)
|
||||
if (viewModel.isLoading) {
|
||||
Text("Loading...")
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ClickableText(
|
||||
text = AnnotatedString("Reset"),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.resetSelfHostingDetails(context) },
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
ClickableText(
|
||||
text = AnnotatedString("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("Learn more about self-hosting Omnivore"),
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SelfHostedFields(
|
||||
apiServer: String,
|
||||
webServer: String,
|
||||
onAPIServerChange: (String) -> Unit,
|
||||
onWebServerChange: (String) -> Unit,
|
||||
onSaveClick: () -> 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 = apiServer,
|
||||
placeholder = { Text(text = "https://api-prod.omnivore.app/") },
|
||||
label = { Text(text = "API Server") },
|
||||
onValueChange = onAPIServerChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Uri,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = webServer,
|
||||
placeholder = { Text(text = "https://omnivore.app/") },
|
||||
label = { Text(text = "Web Server") },
|
||||
onValueChange = onWebServerChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Uri,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
||||
)
|
||||
|
||||
Button(onClick = {
|
||||
if (apiServer.isNotBlank() && webServer.isNotBlank()) {
|
||||
onSaveClick()
|
||||
focusManager.clearFocus()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Please enter API Server and Web server addresses.",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}, colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(0xFF3D3D3D),
|
||||
containerColor = Color(0xffffd234)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Save",
|
||||
modifier = Modifier.padding(horizontal = 100.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -68,6 +68,9 @@ fun WelcomeScreenContent(viewModel: LoginViewModel) {
|
||||
RegistrationState.EmailSignUp -> {
|
||||
EmailSignUpView(viewModel = viewModel)
|
||||
}
|
||||
RegistrationState.SelfHosted -> {
|
||||
SelfHostedView(viewModel = viewModel)
|
||||
}
|
||||
RegistrationState.SocialLogin -> {
|
||||
Text(
|
||||
text = stringResource(id = R.string.welcome_title),
|
||||
@ -137,6 +140,17 @@ fun AuthProviderView(viewModel: LoginViewModel) {
|
||||
.plus(TextStyle(textDecoration = TextDecoration.Underline)),
|
||||
onClick = { viewModel.showEmailSignIn() }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString("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))
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
|
||||
Reference in New Issue
Block a user