diff --git a/app/src/main/java/com/hippo/ehviewer/ui/compose/CookieSignInScreen.kt b/app/src/main/java/com/hippo/ehviewer/ui/compose/CookieSignInScreen.kt index cce73d696..fa56d58a0 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/compose/CookieSignInScreen.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/compose/CookieSignInScreen.kt @@ -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(null) } + + var showErrorDialog by rememberSaveable { mutableStateOf(false) } + var loginErrorException by remember { mutableStateOf(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 = 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 = 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() + ) + } + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/hippo/ehviewer/ui/compose/SignInScreen.kt b/app/src/main/java/com/hippo/ehviewer/ui/compose/SignInScreen.kt index c5560c079..37e10e75d 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/compose/SignInScreen.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/compose/SignInScreen.kt @@ -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(null) } + var loginErrorException by remember { mutableStateOf(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(null) } + var signInJob by remember { mutableStateOf(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