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:
Cuong-Tran 2025-05-03 23:50:53 +07:00 committed by GitHub
parent 4e98b75a02
commit 29cf391ede
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 189 additions and 57 deletions

View file

@ -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))
} }
}, },
) )

View 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>