Refine CookieSignInScreen

This commit is contained in:
tarsin committed 2023-01-11 11:44:13 +08:00
1 parent 9159f4616f
commit 9284008afe
2 files changed
+215 -25

No files matched your search

@@ -9,10 +9,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
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.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
@@ -20,28 +26,169 @@ import androidx.compose.material3.SnackbarHostState
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.remember
import androidx.compose.runtime.rememberCoroutineScope
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.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhClient.METHOD_GET_PROFILE
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.LoginActivity.Companion.SELECT_SITE_ROUTE_NAME
import com.hippo.ehviewer.ui.scene.getProfile
import com.hippo.util.ExceptionUtils
import com.hippo.util.getClipboardManager
import com.hippo.util.getTextFromClipboard
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.Cookie
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CookieSignInScene(navController: NavController) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
var isProgressIndicatorVisible by rememberSaveable { mutableStateOf(false) }
var ipbMemberId by rememberSaveable { mutableStateOf("") }
var ipbPassHash by rememberSaveable { mutableStateOf("") }
var igneous by rememberSaveable { mutableStateOf("") }
var ipbMemberIdErrorState by rememberSaveable { mutableStateOf(false) }
var ipbPassHashErrorState by rememberSaveable { mutableStateOf(false) }
var signInJob by remember { mutableStateOf<Job?>(null) }
var showErrorDialog by rememberSaveable { mutableStateOf(false) }
var loginErrorException by remember { mutableStateOf<Throwable?>(null) }
val noCookies = stringResource(R.string.from_clipboard_error)
fun newCookie(name: String, value: String, domain: String): Cookie {
return Cookie.Builder().name(name).value(value).domain(domain).expiresAt(Long.MAX_VALUE)
.build()
}
fun storeCookie(id: String, hash: String, igneous: String) {
EhUtils.signOut()
val store = EhApplication.ehCookieStore
store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_E))
store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_EX))
store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_E))
store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_EX))
if (igneous.isBlank()) return
store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_E))
store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX))
}
fun login() {
signInJob?.cancel()
if (ipbMemberId.isBlank()) {
ipbMemberIdErrorState = true
return
} else {
ipbMemberIdErrorState = false
}
if (ipbPassHash.isBlank()) {
ipbPassHashErrorState = true
return
} else {
ipbPassHashErrorState = false
}
focusManager.clearFocus()
storeCookie(ipbMemberId, ipbPassHash, igneous)
isProgressIndicatorVisible = true
signInJob = coroutineScope.launchIO {
runCatching {
EhClient.execute(METHOD_GET_PROFILE)
}.onSuccess {
launchIO {
getProfile()
}
withUIContext {
navController.navigate(SELECT_SITE_ROUTE_NAME)
}
}.onFailure {
EhApplication.ehCookieStore.signOut()
if (it !is CancellationException) {
loginErrorException = it
showErrorDialog = true
isProgressIndicatorVisible = false
}
}
}
}
fun fillCookiesFromClipboard() {
focusManager.clearFocus()
val text = context.getClipboardManager().getTextFromClipboard(context)
if (text == null) {
coroutineScope.launch { snackbarHostState.showSnackbar(noCookies) }
return
}
runCatching {
val kvs: Array<String> = if (text.contains(";")) {
text.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} else if (text.contains("\n")) {
text.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} else {
coroutineScope.launch { snackbarHostState.showSnackbar(noCookies) }
return
}
if (kvs.size < 3) {
coroutineScope.launch { snackbarHostState.showSnackbar(noCookies) }
return
}
for (s in kvs) {
val kv: Array<String> = if (s.contains("=")) {
s.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} else if (s.contains(":")) {
s.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} else {
continue
}
if (kv.size != 2) {
continue
}
when (kv[0].trim { it <= ' ' }.lowercase(Locale.getDefault())) {
"ipb_member_id" -> ipbMemberId = kv[1].trim { it <= ' ' }
"ipb_pass_hash" -> ipbPassHash = kv[1].trim { it <= ' ' }
"igneous" -> igneous = kv[1].trim { it <= ' ' }
}
}
login()
}.onFailure {
it.printStackTrace()
coroutineScope.launch { snackbarHostState.showSnackbar(noCookies) }
}
}
Box(
contentAlignment = Alignment.Center
) {
@@ -49,11 +196,11 @@ fun CookieSignInScene(navController: NavController) {
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) {
) { padding ->
Column(
Modifier
.padding(dimensionResource(id = R.dimen.keyline_margin))
.padding(it)
.padding(padding)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
@@ -73,44 +220,59 @@ fun CookieSignInScene(navController: NavController) {
fontSize = 16.sp
)
OutlinedTextField(
value = "",
onValueChange = { },
value = ipbMemberId,
onValueChange = { ipbMemberId = it.trim { char -> char <= ' ' } },
Modifier.width(dimensionResource(id = R.dimen.single_max_width)),
label = {
Text(text = "")
label = { Text(text = "ipb_member_id") },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next
),
supportingText = { if (ipbMemberIdErrorState) Text(stringResource(R.string.text_is_empty)) },
trailingIcon = {
if (ipbMemberIdErrorState) Icon(
imageVector = Icons.Filled.Info,
contentDescription = null
)
},
isError = ipbMemberIdErrorState,
singleLine = true
)
OutlinedTextField(
value = "",
onValueChange = { },
value = ipbPassHash,
onValueChange = { ipbPassHash = it.trim { char -> char <= ' ' } },
Modifier.width(dimensionResource(id = R.dimen.single_max_width)),
label = {
Text(text = "")
label = { Text(text = "ipb_pass_hash") },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next
),
supportingText = { if (ipbPassHashErrorState) Text(stringResource(R.string.text_is_empty)) },
trailingIcon = {
if (ipbPassHashErrorState) Icon(
imageVector = Icons.Filled.Info,
contentDescription = null
)
},
isError = ipbPassHashErrorState,
singleLine = true
)
OutlinedTextField(
value = "",
onValueChange = { },
value = igneous,
onValueChange = { igneous = it.trim { char -> char <= ' ' } },
Modifier.width(dimensionResource(id = R.dimen.single_max_width)),
label = {
Text(text = "")
},
label = { Text(text = "igneous") },
keyboardActions = KeyboardActions(
onDone = { login() }
),
singleLine = true
)
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = {
coroutineScope.launch {
snackbarHostState.showSnackbar("1")
}
},
onClick = { login() },
Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = android.R.string.ok))
}
TextButton(onClick = { /*TODO*/ }) {
TextButton(onClick = { fillCookiesFromClipboard() }) {
Text(
text = buildAnnotatedString {
withStyle(
@@ -125,6 +287,33 @@ fun CookieSignInScene(navController: NavController) {
}
}
}
CircularProgressIndicator()
if (isProgressIndicatorVisible)
CircularProgressIndicator()
if (showErrorDialog) {
AlertDialog(
onDismissRequest = {
showErrorDialog = false
},
confirmButton = {
TextButton(onClick = {
showErrorDialog = false
}) {
Text(text = stringResource(id = R.string.get_it))
}
},
title = {
Text(text = stringResource(id = R.string.sign_in_failed))
},
text = {
Text(
text =
"""
${ExceptionUtils.getReadableString(loginErrorException!!)}
${stringResource(R.string.wrong_cookie_warning)}
""".trimIndent()
)
}
)
}
}
}
@@ -31,6 +31,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@@ -77,12 +78,12 @@ fun SignInScreen(navController: NavController) {
var showUsernameError by rememberSaveable { mutableStateOf(false) }
var showPasswordError by rememberSaveable { mutableStateOf(false) }
var showErrorDialog by rememberSaveable { mutableStateOf(false) }
var loginErrorException by rememberSaveable { mutableStateOf<Throwable?>(null) }
var loginErrorException by remember { mutableStateOf<Throwable?>(null) }
var username by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
var passwordHidden by rememberSaveable { mutableStateOf(true) }
val context = LocalContext.current
var signInJob by rememberSaveable { mutableStateOf<Job?>(null) }
var signInJob by remember { mutableStateOf<Job?>(null) }
// Basic login request
fun signIn() {
@@ -113,8 +114,8 @@ fun SignInScreen(navController: NavController) {
withUIContext {
focusManager.clearFocus()
isProgressIndicatorVisible = false
showErrorDialog = true
loginErrorException = it
showErrorDialog = true
}
}.onSuccess {
// This composable is to be finished