feat(tracker-login): Enhance tracking login dialog with autofill and loading state (#884)
* fix(tracker-login): allow autofill on tracker's login dialog * add loading state to tracking login dialog * improve error color handling and dynamic height measurement in login dialog * enable autofill for email and password fields in login dialog * improve code * refactor: optimize state management and improve dialog width measurement * refactor: optimize dialog height measurement and error state handling
This commit is contained in:
parent
4e98b75a02
commit
29cf391ede
2 changed files with 189 additions and 57 deletions
|
|
@ -1,16 +1,17 @@
|
||||||
package eu.kanade.presentation.more.settings.screen
|
package eu.kanade.presentation.more.settings.screen
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||||
import androidx.compose.material.icons.filled.Visibility
|
|
||||||
import androidx.compose.material.icons.filled.VisibilityOff
|
|
||||||
import androidx.compose.material.icons.outlined.Close
|
import androidx.compose.material.icons.outlined.Close
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
|
@ -19,7 +20,6 @@ import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
|
@ -30,19 +30,19 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
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.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.domain.track.model.AutoTrackState
|
import eu.kanade.domain.track.model.AutoTrackState
|
||||||
import eu.kanade.domain.track.service.TrackPreferences
|
import eu.kanade.domain.track.service.TrackPreferences
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
|
import eu.kanade.presentation.theme.colorscheme.AndroidViewColorScheme
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
|
|
@ -50,6 +50,7 @@ import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
|
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
||||||
|
import eu.kanade.tachiyomi.databinding.DialogTrackingLoginBinding
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -62,6 +63,7 @@ import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.kmk.KMR
|
import tachiyomi.i18n.kmk.KMR
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
|
@ -89,7 +91,6 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||||
val trackPreferences = remember { Injekt.get<TrackPreferences>() }
|
val trackPreferences = remember { Injekt.get<TrackPreferences>() }
|
||||||
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
||||||
val sourceManager = remember { Injekt.get<SourceManager>() }
|
val sourceManager = remember { Injekt.get<SourceManager>() }
|
||||||
val autoTrackStatePref = trackPreferences.autoUpdateTrackOnMarkRead()
|
|
||||||
|
|
||||||
var dialog by remember { mutableStateOf<Any?>(null) }
|
var dialog by remember { mutableStateOf<Any?>(null) }
|
||||||
dialog?.run {
|
dialog?.run {
|
||||||
|
|
@ -204,10 +205,19 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
var username by remember { mutableStateOf(TextFieldValue(tracker.getUsername())) }
|
// KMK -->
|
||||||
var password by remember { mutableStateOf(TextFieldValue(tracker.getPassword())) }
|
val usernameHint = stringResource(uNameStringRes)
|
||||||
|
|
||||||
|
var username by remember(tracker) { mutableStateOf(tracker.getUsername()) }
|
||||||
|
var password by remember(tracker) { mutableStateOf(tracker.getPassword()) }
|
||||||
var processing by remember { mutableStateOf(false) }
|
var processing by remember { mutableStateOf(false) }
|
||||||
var inputError by remember { mutableStateOf(false) }
|
var inputError by remember { mutableStateOf(false) }
|
||||||
|
val colorScheme = AndroidViewColorScheme(MaterialTheme.colorScheme)
|
||||||
|
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
var measuredHeightDp by remember(configuration) { mutableStateOf(0.dp) }
|
||||||
|
// KMK <--
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
|
@ -226,70 +236,143 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
// KMK -->
|
||||||
OutlinedTextField(
|
if (processing) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
LoadingScreen(
|
||||||
value = username,
|
modifier = Modifier.heightIn(max = measuredHeightDp),
|
||||||
onValueChange = { username = it },
|
|
||||||
label = { Text(text = stringResource(uNameStringRes)) },
|
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
|
||||||
singleLine = true,
|
|
||||||
isError = inputError && !processing,
|
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
AndroidView(
|
||||||
|
factory = { factoryContext ->
|
||||||
|
val binding = DialogTrackingLoginBinding.inflate(LayoutInflater.from(factoryContext))
|
||||||
|
|
||||||
var hidePassword by remember { mutableStateOf(true) }
|
// Measure dialog height for loading state
|
||||||
OutlinedTextField(
|
if (measuredHeightDp == 0.dp) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||||
value = password,
|
val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||||
onValueChange = { password = it },
|
binding.root.measure(widthMeasureSpec, heightMeasureSpec)
|
||||||
label = { Text(text = stringResource(MR.strings.password)) },
|
with(density) { measuredHeightDp = binding.root.measuredHeight.toDp() }
|
||||||
trailingIcon = {
|
}
|
||||||
IconButton(onClick = { hidePassword = !hidePassword }) {
|
|
||||||
Icon(
|
val usernameInputLayout = binding.usernameInputLayout
|
||||||
imageVector = if (hidePassword) {
|
val usernameEditText = binding.usernameEditText
|
||||||
Icons.Filled.Visibility
|
val passwordEditText = binding.passwordEditText
|
||||||
} else {
|
|
||||||
Icons.Filled.VisibilityOff
|
// Apply color scheme and selection handles
|
||||||
},
|
listOf(usernameEditText, passwordEditText).forEach { editText ->
|
||||||
contentDescription = null,
|
editText.setTextColor(colorScheme.textColor)
|
||||||
|
editText.highlightColor = colorScheme.textHighlightColor
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
editText.textSelectHandle?.let { drawable ->
|
||||||
|
drawable.setTint(colorScheme.primary)
|
||||||
|
editText.setTextSelectHandle(drawable)
|
||||||
|
}
|
||||||
|
editText.textSelectHandleLeft?.let { drawable ->
|
||||||
|
drawable.setTint(colorScheme.primary)
|
||||||
|
editText.setTextSelectHandleLeft(drawable)
|
||||||
|
}
|
||||||
|
editText.textSelectHandleRight?.let { drawable ->
|
||||||
|
drawable.setTint(colorScheme.primary)
|
||||||
|
editText.setTextSelectHandleRight(drawable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameInputLayout.hint = usernameHint
|
||||||
|
usernameEditText.setText(username)
|
||||||
|
passwordEditText.setText(password)
|
||||||
|
|
||||||
|
// Clear errors and update Compose state on text change
|
||||||
|
usernameEditText.doAfterTextChanged {
|
||||||
|
val newValue = it?.toString().orEmpty()
|
||||||
|
if (username != newValue) {
|
||||||
|
username = newValue
|
||||||
|
inputError = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
passwordEditText.doAfterTextChanged {
|
||||||
|
val newValue = it?.toString().orEmpty()
|
||||||
|
if (password != newValue) {
|
||||||
|
password = newValue
|
||||||
|
inputError = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.root
|
||||||
|
},
|
||||||
|
update = { view ->
|
||||||
|
val binding = DialogTrackingLoginBinding.bind(view)
|
||||||
|
val usernameInputLayout = binding.usernameInputLayout
|
||||||
|
val passwordInputLayout = binding.passwordInputLayout
|
||||||
|
|
||||||
|
val (strokeColorFocused, strokeColorDefault, hintColor, cursorColor) = if (inputError) {
|
||||||
|
arrayOf(
|
||||||
|
colorScheme.error,
|
||||||
|
colorScheme.error,
|
||||||
|
colorScheme.error,
|
||||||
|
colorScheme.error,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
arrayOf(
|
||||||
|
colorScheme.primary,
|
||||||
|
colorScheme.onSurfaceVariant,
|
||||||
|
colorScheme.primary,
|
||||||
|
colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val boxStrokeColorStateList = ColorStateList(
|
||||||
|
arrayOf(
|
||||||
|
intArrayOf(android.R.attr.state_focused),
|
||||||
|
intArrayOf(), // Default state
|
||||||
|
),
|
||||||
|
intArrayOf(
|
||||||
|
strokeColorFocused,
|
||||||
|
strokeColorDefault,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val hintTextColorStateList = ColorStateList.valueOf(hintColor)
|
||||||
|
val endIconTintList = ColorStateList.valueOf(colorScheme.onSurfaceVariant)
|
||||||
|
val cursorColorStateList = ColorStateList.valueOf(cursorColor)
|
||||||
|
|
||||||
|
listOf(usernameInputLayout, passwordInputLayout).forEach {
|
||||||
|
it.setBoxStrokeColorStateList(boxStrokeColorStateList)
|
||||||
|
it.hintTextColor = hintTextColorStateList
|
||||||
|
it.setEndIconTintList(endIconTintList)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
it.cursorColor = cursorColorStateList
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
visualTransformation = if (hidePassword) {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
PasswordVisualTransformation()
|
|
||||||
} else {
|
|
||||||
VisualTransformation.None
|
|
||||||
},
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
keyboardType = KeyboardType.Password,
|
|
||||||
imeAction = ImeAction.Done,
|
|
||||||
),
|
|
||||||
singleLine = true,
|
|
||||||
isError = inputError && !processing,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// KMK <--
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled = !processing && username.text.isNotBlank() && password.text.isNotBlank(),
|
enabled = !processing && username.isNotBlank() && password.isNotBlank(),
|
||||||
onClick = {
|
onClick = {
|
||||||
|
// KMK -->
|
||||||
|
processing = true
|
||||||
|
inputError = false // Clear previous error before check
|
||||||
|
// KMK <--
|
||||||
scope.launchIO {
|
scope.launchIO {
|
||||||
processing = true
|
val success = checkLogin(
|
||||||
val result = checkLogin(
|
|
||||||
context = context,
|
context = context,
|
||||||
tracker = tracker,
|
tracker = tracker,
|
||||||
username = username.text,
|
username = username,
|
||||||
password = password.text,
|
password = password,
|
||||||
)
|
)
|
||||||
inputError = !result
|
inputError = !success
|
||||||
if (result) onDismissRequest()
|
if (success) onDismissRequest()
|
||||||
processing = false
|
processing = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
val id = if (processing) MR.strings.loading else MR.strings.login
|
Text(text = stringResource(if (processing) MR.strings.loading else MR.strings.login))
|
||||||
Text(text = stringResource(id))
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
49
app/src/main/res/layout/dialog_tracking_login.xml
Normal file
49
app/src/main/res/layout/dialog_tracking_login.xml
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/username_input_layout"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/username">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/username_edit_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:autofillHints="username,emailAddress"
|
||||||
|
android:inputType="textEmailAddress"
|
||||||
|
android:imeOptions="actionNext"
|
||||||
|
android:nextFocusDown="@id/password_edit_text"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:importantForAutofill="yes" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/password_input_layout"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:hint="@string/password"
|
||||||
|
app:endIconMode="password_toggle">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/password_edit_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:autofillHints="password"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:importantForAutofill="yes" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
Loading…
Reference in a new issue