mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-04-14 11:46:32 +00:00
add self-host options to login
This commit is contained in:
parent
2335bff814
commit
df561f1fff
8 changed files with 314 additions and 24 deletions
|
|
@ -1641,4 +1641,19 @@
|
||||||
<item quantity="one">%d Item pending</item>
|
<item quantity="one">%d Item pending</item>
|
||||||
<item quantity="other">%d Items pending</item>
|
<item quantity="other">%d Items pending</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="custom_server_content_description">Server options</string>
|
||||||
|
<string name="custom_server_title">Server configuration</string>
|
||||||
|
<string name="custom_server_label">Server URL</string>
|
||||||
|
<string name="custom_server_placeholder">https://habitica.com</string>
|
||||||
|
<string name="custom_server_apply">Apply</string>
|
||||||
|
<string name="custom_server_reset">Reset to default</string>
|
||||||
|
<string name="custom_server_invalid">Couldn’t understand that server address. Include http:// or https:// and a host name.</string>
|
||||||
|
<string name="dev_options_warning">Warning: Only use these developer settings if you know what you’re doing.</string>
|
||||||
|
<plurals name="dev_options_taps_remaining">
|
||||||
|
<item quantity="one">Developer options unlock in %d tap.</item>
|
||||||
|
<item quantity="other">Developer options unlock in %d taps.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="dev_options_unlocked">Developer options unlocked.</string>
|
||||||
|
<string name="custom_up_server_label">UnifiedPush server URL</string>
|
||||||
|
<string name="custom_up_server_placeholder">https://example.com</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -76,14 +76,19 @@ object Analytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initialize(context: Context) {
|
fun initialize(context: Context) {
|
||||||
|
val amplitudeAppId = context.getString(R.string.amplitude_app_id)
|
||||||
|
if (amplitudeAppId.isNullOrBlank()) {
|
||||||
|
// No amplitude configuration provided; skip amplitude setup for this build.
|
||||||
|
} else {
|
||||||
amplitude =
|
amplitude =
|
||||||
Amplitude(
|
Amplitude(
|
||||||
Configuration(
|
Configuration(
|
||||||
context.getString(R.string.amplitude_app_id),
|
amplitudeAppId,
|
||||||
context,
|
context,
|
||||||
optOut = true,
|
optOut = true,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
firebase = FirebaseAnalytics.getInstance(context)
|
firebase = FirebaseAnalytics.getInstance(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ class PushNotificationManager(
|
||||||
// catchy catch
|
// catchy catch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
addUnifiedPushDeviceIfConfigured()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addRefreshToken() {
|
private fun addRefreshToken() {
|
||||||
|
|
@ -90,6 +91,22 @@ class PushNotificationManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addUnifiedPushDeviceIfConfigured() {
|
||||||
|
val unifiedPushUrl = sharedPreferences.getString(UNIFIED_PUSH_SERVER_KEY, null)?.trim()
|
||||||
|
if (unifiedPushUrl.isNullOrEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.user == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val pushDeviceData = HashMap<String, String>()
|
||||||
|
pushDeviceData["regId"] = unifiedPushUrl
|
||||||
|
pushDeviceData["type"] = "unifiedpush"
|
||||||
|
MainScope().launchCatching {
|
||||||
|
apiClient.addPushDevice(pushDeviceData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun removePushDeviceUsingStoredToken() {
|
suspend fun removePushDeviceUsingStoredToken() {
|
||||||
if (this.refreshedToken.isEmpty() || !userHasPushDevice()) {
|
if (this.refreshedToken.isEmpty() || !userHasPushDevice()) {
|
||||||
return
|
return
|
||||||
|
|
@ -140,6 +157,7 @@ class PushNotificationManager(
|
||||||
const val CONTENT_RELEASE_NOTIFICATION_KEY = "contentRelease"
|
const val CONTENT_RELEASE_NOTIFICATION_KEY = "contentRelease"
|
||||||
const val G1G1_PROMO_KEY = "g1g1Promo"
|
const val G1G1_PROMO_KEY = "g1g1Promo"
|
||||||
const val DEVICE_TOKEN_PREFERENCE_KEY = "device-token-preference"
|
const val DEVICE_TOKEN_PREFERENCE_KEY = "device-token-preference"
|
||||||
|
private const val UNIFIED_PUSH_SERVER_KEY = "unified_push_server_url"
|
||||||
|
|
||||||
fun displayNotification(
|
fun displayNotification(
|
||||||
remoteMessage: RemoteMessage,
|
remoteMessage: RemoteMessage,
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ class OnboardingActivity: BaseActivity() {
|
||||||
|
|
||||||
val authenticationViewModel: AuthenticationViewModel by viewModels()
|
val authenticationViewModel: AuthenticationViewModel by viewModels()
|
||||||
|
|
||||||
val currentStep = mutableStateOf(OnboardingSteps.SETUP)
|
val currentStep = mutableStateOf(OnboardingSteps.LOGIN)
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var configManager: AppConfigManager
|
lateinit var configManager: AppConfigManager
|
||||||
|
|
@ -91,6 +91,11 @@ class OnboardingActivity: BaseActivity() {
|
||||||
// Set default values to avoid null-responses when requesting unedited settings
|
// Set default values to avoid null-responses when requesting unedited settings
|
||||||
PreferenceManager.setDefaultValues(this, R.xml.preferences_fragment, false)
|
PreferenceManager.setDefaultValues(this, R.xml.preferences_fragment, false)
|
||||||
|
|
||||||
|
if (authenticationViewModel.hostConfig.hasAuthentication()) {
|
||||||
|
startMainActivity()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
binding.composeView.setContent {
|
binding.composeView.setContent {
|
||||||
val step by currentStep
|
val step by currentStep
|
||||||
HabiticaTheme {
|
HabiticaTheme {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.tasks.await
|
import kotlinx.coroutines.tasks.await
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class AuthenticationViewModel @Inject constructor(
|
class AuthenticationViewModel @Inject constructor(
|
||||||
|
|
@ -78,6 +79,66 @@ class AuthenticationViewModel @Inject constructor(
|
||||||
private var _usernameIssues = MutableStateFlow<String?>(null)
|
private var _usernameIssues = MutableStateFlow<String?>(null)
|
||||||
val usernameIssues: Flow<String?> = _usernameIssues
|
val usernameIssues: Flow<String?> = _usernameIssues
|
||||||
|
|
||||||
|
fun currentServerSelection(): String {
|
||||||
|
return sharedPrefs.getString(SERVER_OVERRIDE_KEY, hostConfig.address) ?: hostConfig.address
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentUnifiedPushServer(): String {
|
||||||
|
return sharedPrefs.getString(UNIFIED_PUSH_SERVER_KEY, "") ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDevOptionsUnlocked(): Boolean {
|
||||||
|
return sharedPrefs.getBoolean(DEV_OPTIONS_UNLOCKED_KEY, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDevOptionsUnlocked(unlocked: Boolean) {
|
||||||
|
sharedPrefs.edit {
|
||||||
|
putBoolean(DEV_OPTIONS_UNLOCKED_KEY, unlocked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyServerOverride(serverUrl: String?): Boolean {
|
||||||
|
val normalized = normalizeServerUrl(serverUrl)
|
||||||
|
?: return false
|
||||||
|
|
||||||
|
sharedPrefs.edit {
|
||||||
|
putString(SERVER_OVERRIDE_KEY, normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiClient.updateServerUrl(normalized)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateUnifiedPushServer(serverUrl: String?) {
|
||||||
|
val sanitized = serverUrl?.trim()?.takeIf { it.isNotEmpty() }
|
||||||
|
sharedPrefs.edit {
|
||||||
|
if (sanitized != null) {
|
||||||
|
putString(UNIFIED_PUSH_SERVER_KEY, sanitized)
|
||||||
|
} else {
|
||||||
|
remove(UNIFIED_PUSH_SERVER_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetServerOverride() {
|
||||||
|
sharedPrefs.edit {
|
||||||
|
remove(SERVER_OVERRIDE_KEY)
|
||||||
|
remove(UNIFIED_PUSH_SERVER_KEY)
|
||||||
|
if (!isDevOptionsUnlocked()) {
|
||||||
|
remove(DEV_OPTIONS_UNLOCKED_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiClient.updateServerUrl(BuildConfig.BASE_URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeServerUrl(input: String?): String? {
|
||||||
|
val trimmed = input?.trim()?.takeIf { it.isNotEmpty() } ?: return null
|
||||||
|
val candidate = if (trimmed.contains("://")) trimmed else "https://$trimmed"
|
||||||
|
val httpUrl = runCatching { candidate.toHttpUrlOrNull() }.getOrNull() ?: return null
|
||||||
|
val rebuilt = httpUrl.newBuilder().encodedPath("/").build().toString()
|
||||||
|
return rebuilt.trimEnd('/')
|
||||||
|
}
|
||||||
|
|
||||||
fun validateInputs(
|
fun validateInputs(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
|
@ -273,4 +334,10 @@ class AuthenticationViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SERVER_OVERRIDE_KEY = "server_url"
|
||||||
|
private const val UNIFIED_PUSH_SERVER_KEY = "unified_push_server_url"
|
||||||
|
private const val DEV_OPTIONS_UNLOCKED_KEY = "dev_options_unlocked"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.habitrpg.android.habitica.ui.views.login
|
package com.habitrpg.android.habitica.ui.views.login
|
||||||
|
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.EaseInOut
|
import androidx.compose.animation.core.EaseInOut
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
|
@ -13,6 +14,7 @@ import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
|
@ -22,10 +24,16 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.ProvideTextStyle
|
import androidx.compose.material3.ProvideTextStyle
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
|
@ -38,6 +46,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -74,6 +83,24 @@ fun LoginScreen(authenticationViewModel: AuthenticationViewModel, onNextOnboardi
|
||||||
var passwordFieldState by remember { mutableStateOf(LoginFieldState.DEFAULT) }
|
var passwordFieldState by remember { mutableStateOf(LoginFieldState.DEFAULT) }
|
||||||
var email by authenticationViewModel.email
|
var email by authenticationViewModel.email
|
||||||
var emailFieldState by remember { mutableStateOf(LoginFieldState.DEFAULT) }
|
var emailFieldState by remember { mutableStateOf(LoginFieldState.DEFAULT) }
|
||||||
|
var showServerDialog by remember { mutableStateOf(false) }
|
||||||
|
var customServerUrl by remember { mutableStateOf(authenticationViewModel.currentServerSelection()) }
|
||||||
|
var unifiedPushUrl by remember { mutableStateOf(authenticationViewModel.currentUnifiedPushServer()) }
|
||||||
|
var serverError by remember { mutableStateOf<String?>(null) }
|
||||||
|
val invalidServerMessage = stringResource(R.string.custom_server_invalid)
|
||||||
|
val context = LocalContext.current
|
||||||
|
var devTapCount by remember { mutableStateOf(0) }
|
||||||
|
var devOptionsUnlocked by remember { mutableStateOf(authenticationViewModel.isDevOptionsUnlocked()) }
|
||||||
|
val requiredTapCount = 7
|
||||||
|
|
||||||
|
LaunchedEffect(showServerDialog) {
|
||||||
|
if (showServerDialog) {
|
||||||
|
customServerUrl = authenticationViewModel.currentServerSelection()
|
||||||
|
unifiedPushUrl = authenticationViewModel.currentUnifiedPushServer()
|
||||||
|
serverError = null
|
||||||
|
devTapCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
Box(modifier.fillMaxSize()) {
|
Box(modifier.fillMaxSize()) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
|
|
@ -125,6 +152,40 @@ fun LoginScreen(authenticationViewModel: AuthenticationViewModel, onNextOnboardi
|
||||||
Image(painterResource(R.drawable.arrow_back), contentDescription = null)
|
Image(painterResource(R.drawable.arrow_back), contentDescription = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Button(
|
||||||
|
{
|
||||||
|
if (showServerDialog) {
|
||||||
|
return@Button
|
||||||
|
}
|
||||||
|
if (devOptionsUnlocked) {
|
||||||
|
showServerDialog = true
|
||||||
|
return@Button
|
||||||
|
}
|
||||||
|
val nextCount = devTapCount + 1
|
||||||
|
if (nextCount >= requiredTapCount) {
|
||||||
|
devTapCount = 0
|
||||||
|
devOptionsUnlocked = true
|
||||||
|
authenticationViewModel.setDevOptionsUnlocked(true)
|
||||||
|
Toast.makeText(context, context.getString(R.string.dev_options_unlocked), Toast.LENGTH_SHORT).show()
|
||||||
|
showServerDialog = true
|
||||||
|
} else {
|
||||||
|
devTapCount = nextCount
|
||||||
|
val remaining = requiredTapCount - nextCount
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.resources.getQuantityString(R.plurals.dev_options_taps_remaining, remaining, remaining),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.textButtonColors(contentColor = Color.White),
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd).padding(WindowInsets.systemBars.asPaddingValues())
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painterResource(R.drawable.menu_settings),
|
||||||
|
contentDescription = stringResource(R.string.custom_server_content_description)
|
||||||
|
)
|
||||||
|
}
|
||||||
val logoPadding by animateDpAsState(
|
val logoPadding by animateDpAsState(
|
||||||
if (loginScreenState == LoginScreenState.INITIAL) {
|
if (loginScreenState == LoginScreenState.INITIAL) {
|
||||||
120.dp
|
120.dp
|
||||||
|
|
@ -231,4 +292,111 @@ fun LoginScreen(authenticationViewModel: AuthenticationViewModel, onNextOnboardi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (showServerDialog) {
|
||||||
|
val dialogContainer = Color(0xFF3B3B3B)
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showServerDialog = false },
|
||||||
|
title = { Text(stringResource(R.string.custom_server_title)) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.dev_options_warning),
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp)
|
||||||
|
)
|
||||||
|
val containerColor = Color(0xFF3B3B3B)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = customServerUrl,
|
||||||
|
onValueChange = { customServerUrl = it },
|
||||||
|
label = { Text(stringResource(R.string.custom_server_label), color = Color.White) },
|
||||||
|
placeholder = { Text(stringResource(R.string.custom_server_placeholder), color = Color.White.copy(alpha = 0.6f)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = containerColor,
|
||||||
|
unfocusedContainerColor = containerColor,
|
||||||
|
focusedLabelColor = Color.White,
|
||||||
|
unfocusedLabelColor = Color.White,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
cursorColor = MaterialTheme.colorScheme.primary,
|
||||||
|
focusedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
unfocusedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
focusedPlaceholderColor = Color.White.copy(alpha = 0.6f),
|
||||||
|
unfocusedPlaceholderColor = Color.White.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (serverError != null) {
|
||||||
|
Text(
|
||||||
|
serverError!!,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = unifiedPushUrl,
|
||||||
|
onValueChange = { unifiedPushUrl = it },
|
||||||
|
label = { Text(stringResource(R.string.custom_up_server_label), color = Color.White) },
|
||||||
|
placeholder = { Text(stringResource(R.string.custom_up_server_placeholder), color = Color.White.copy(alpha = 0.6f)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = containerColor,
|
||||||
|
unfocusedContainerColor = containerColor,
|
||||||
|
focusedLabelColor = Color.White,
|
||||||
|
unfocusedLabelColor = Color.White,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
cursorColor = MaterialTheme.colorScheme.primary,
|
||||||
|
focusedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
unfocusedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
focusedPlaceholderColor = Color.White.copy(alpha = 0.6f),
|
||||||
|
unfocusedPlaceholderColor = Color.White.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = dialogContainer,
|
||||||
|
tonalElevation = 8.dp,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
val applied = authenticationViewModel.applyServerOverride(customServerUrl)
|
||||||
|
if (applied) {
|
||||||
|
authenticationViewModel.updateUnifiedPushServer(unifiedPushUrl)
|
||||||
|
serverError = null
|
||||||
|
showServerDialog = false
|
||||||
|
} else {
|
||||||
|
serverError = invalidServerMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.custom_server_apply))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
Row {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
authenticationViewModel.resetServerOverride()
|
||||||
|
authenticationViewModel.updateUnifiedPushServer(null)
|
||||||
|
customServerUrl = authenticationViewModel.currentServerSelection()
|
||||||
|
unifiedPushUrl = authenticationViewModel.currentUnifiedPushServer()
|
||||||
|
serverError = null
|
||||||
|
showServerDialog = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.custom_server_reset))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
TextButton(onClick = { showServerDialog = false }) {
|
||||||
|
Text(stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.habitrpg.android.habitica.R
|
import com.habitrpg.android.habitica.R
|
||||||
import com.habitrpg.android.habitica.data.TaskRepository
|
import com.habitrpg.android.habitica.data.TaskRepository
|
||||||
|
|
@ -239,8 +240,17 @@ class YesterdailyDialog private constructor(
|
||||||
taskRepository: TaskRepository
|
taskRepository: TaskRepository
|
||||||
) {
|
) {
|
||||||
if (userRepository != null && userId != null) {
|
if (userRepository != null && userId != null) {
|
||||||
MainScope().launchCatching {
|
val lifecycleOwner = activity as? LifecycleOwner ?: return
|
||||||
|
lifecycleOwner.lifecycleScope.launchCatching {
|
||||||
delay(500.toDuration(DurationUnit.MILLISECONDS))
|
delay(500.toDuration(DurationUnit.MILLISECONDS))
|
||||||
|
val lifecycle = lifecycleOwner.lifecycle
|
||||||
|
if (!lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.STARTED)) {
|
||||||
|
return@launchCatching
|
||||||
|
}
|
||||||
|
val hostActivity = activity
|
||||||
|
if (hostActivity.isFinishing || hostActivity.isDestroyed) {
|
||||||
|
return@launchCatching
|
||||||
|
}
|
||||||
if (userRepository.isClosed) {
|
if (userRepository.isClosed) {
|
||||||
return@launchCatching
|
return@launchCatching
|
||||||
}
|
}
|
||||||
|
|
@ -282,7 +292,7 @@ class YesterdailyDialog private constructor(
|
||||||
displayedDialog =
|
displayedDialog =
|
||||||
WeakReference(
|
WeakReference(
|
||||||
showDialog(
|
showDialog(
|
||||||
activity,
|
hostActivity,
|
||||||
userRepository,
|
userRepository,
|
||||||
taskRepository,
|
taskRepository,
|
||||||
sortedTasks
|
sortedTasks
|
||||||
|
|
@ -307,6 +317,9 @@ class YesterdailyDialog private constructor(
|
||||||
dialog.setCanceledOnTouchOutside(false)
|
dialog.setCanceledOnTouchOutside(false)
|
||||||
if (!activity.isFinishing) {
|
if (!activity.isFinishing) {
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
dialog.setOnDismissListener {
|
||||||
|
displayedDialog = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,20 +22,19 @@ class HostConfig {
|
||||||
|
|
||||||
constructor(sharedPreferences: SharedPreferences, keyHelper: KeyHelper?, context: Context) {
|
constructor(sharedPreferences: SharedPreferences, keyHelper: KeyHelper?, context: Context) {
|
||||||
this.port = BuildConfig.PORT
|
this.port = BuildConfig.PORT
|
||||||
if (BuildConfig.DEBUG) {
|
val storedAddress = sharedPreferences.getString("server_url", null)?.takeIf { it.isNotBlank() }
|
||||||
this.address = BuildConfig.BASE_URL
|
|
||||||
if (BuildConfig.TEST_USER_ID.isNotBlank()) {
|
if (BuildConfig.DEBUG && BuildConfig.TEST_USER_ID.isNotBlank()) {
|
||||||
|
this.address = storedAddress ?: BuildConfig.BASE_URL
|
||||||
userID = BuildConfig.TEST_USER_ID
|
userID = BuildConfig.TEST_USER_ID
|
||||||
apiKey = BuildConfig.TEST_USER_KEY
|
apiKey = BuildConfig.TEST_USER_KEY
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
val address = sharedPreferences.getString("server_url", null)
|
this.address = when {
|
||||||
if (!address.isNullOrEmpty()) {
|
storedAddress != null -> storedAddress
|
||||||
this.address = address
|
BuildConfig.DEBUG -> BuildConfig.BASE_URL
|
||||||
} else {
|
else -> context.getString(com.habitrpg.common.habitica.R.string.base_url)
|
||||||
this.address = context.getString(com.habitrpg.common.habitica.R.string.base_url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.userID = sharedPreferences.getString(context.getString(com.habitrpg.common.habitica.R.string.SP_userID), null) ?: ""
|
this.userID = sharedPreferences.getString(context.getString(com.habitrpg.common.habitica.R.string.SP_userID), null) ?: ""
|
||||||
this.apiKey = loadAPIKey(sharedPreferences, keyHelper)
|
this.apiKey = loadAPIKey(sharedPreferences, keyHelper)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue