diff --git a/app/build.gradle b/app/build.gradle index da3fa6ef..e0cc7b0b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -127,7 +127,6 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2' implementation 'org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7' -// implementation 'androidx.compose.material3:material3:1.0.0' def media3_version = "1.2.1" implementation "androidx.media3:media3-exoplayer:$media3_version" @@ -176,6 +175,7 @@ dependencies { // Optional - Add full set of material icons implementation 'androidx.compose.material:material-icons-extended' implementation 'androidx.compose.ui:ui-util' +// implementation 'androidx.compose.material3:material3:1.0.0' // Android Studio Preview support implementation 'androidx.compose.ui:ui-tooling-preview' diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/login/LoginPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/login/LoginPage.kt new file mode 100644 index 00000000..1ae50e1f --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/login/LoginPage.kt @@ -0,0 +1,322 @@ +package com.huanchengfly.tieba.post.ui.page.login + +import android.annotation.SuppressLint +import android.webkit.CookieManager +import android.webkit.WebView +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.SnackbarDuration +import androidx.compose.material.SnackbarHostState +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.derivedStateOf +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.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import com.huanchengfly.tieba.post.R +import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage +import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme +import com.huanchengfly.tieba.post.ui.page.webview.MyWebChromeClient +import com.huanchengfly.tieba.post.ui.page.webview.MyWebViewClient +import com.huanchengfly.tieba.post.ui.page.webview.isInternalHost +import com.huanchengfly.tieba.post.ui.widgets.compose.BackNavigationIcon +import com.huanchengfly.tieba.post.ui.widgets.compose.ClickMenu +import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad +import com.huanchengfly.tieba.post.ui.widgets.compose.LoadingState +import com.huanchengfly.tieba.post.ui.widgets.compose.LocalSnackbarHostState +import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold +import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar +import com.huanchengfly.tieba.post.ui.widgets.compose.WebView +import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState +import com.huanchengfly.tieba.post.ui.widgets.compose.rememberSaveableWebViewState +import com.huanchengfly.tieba.post.ui.widgets.compose.rememberWebViewNavigator +import com.huanchengfly.tieba.post.utils.AccountUtil +import com.huanchengfly.tieba.post.utils.AccountUtil.parseCookie +import com.huanchengfly.tieba.post.utils.ClientUtils +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +const val LOGIN_URL = + "https://wappass.baidu.com/passport?login&u=https%3A%2F%2Ftieba.baidu.com%2Findex%2Ftbwise%2Fmine" + +@SuppressLint("SetJavaScriptEnabled") +@Destination +@Composable +fun LoginPage( + navigator: DestinationsNavigator, +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val webViewState = rememberSaveableWebViewState() + val webViewNavigator = rememberWebViewNavigator() + var loaded by rememberSaveable { + mutableStateOf(false) + } + var pageTitle by rememberSaveable { + mutableStateOf("") + } + val displayPageTitle by remember { + derivedStateOf { + pageTitle.ifEmpty { + context.getString(R.string.title_default) + } + } + } + val currentHost by remember { + derivedStateOf { + webViewState.lastLoadedUrl?.toUri()?.host.orEmpty().lowercase() + } + } + val isExternalHost by remember { + derivedStateOf { + currentHost.isNotEmpty() && !isInternalHost(currentHost) + } + } + + DisposableEffect(Unit) { + val job = coroutineScope.launch { + snapshotFlow { webViewState.pageTitle } + .filterNotNull() + .filter { it.isNotEmpty() } + .cancellable() + .collect { + pageTitle = it + } + } + onDispose { + job.cancel() + } + } + + LazyLoad(loaded = loaded) { + webViewNavigator.loadUrl(LOGIN_URL) + loaded = true + } + + val isLoading by remember { + derivedStateOf { + webViewState.loadingState is LoadingState.Loading + } + } + + val progress by remember { + derivedStateOf { + webViewState.loadingState.let { + if (it is LoadingState.Loading) { + it.progress + } else { + 0f + } + } + } + } + + val animatedProgress by animateFloatAsState(targetValue = progress, label = "progress") + + MyScaffold( + topBar = { + Toolbar( + title = { + Column { + Text( + text = displayPageTitle, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (isExternalHost) { + Text( + text = currentHost, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = ExtendedTheme.colors.onTopBarSecondary, + style = MaterialTheme.typography.caption + ) + } + } + }, + navigationIcon = { BackNavigationIcon(onBackPressed = { navigator.navigateUp() }) }, + actions = { + val menuState = rememberMenuState() + ClickMenu( + menuContent = { + DropdownMenuItem( + onClick = { + webViewNavigator.reload() + dismiss() + } + ) { + Text(text = stringResource(id = R.string.title_refresh)) + } + }, + menuState = menuState, + triggerShape = CircleShape + ) { + Box( + modifier = Modifier.size(48.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Rounded.MoreVert, + contentDescription = stringResource(id = R.string.btn_more) + ) + } + } + }, + ) + } + ) { paddingValues -> + Box { + val snackbarHostState = LocalSnackbarHostState.current + WebView( + state = webViewState, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + navigator = webViewNavigator, + onCreated = { + it.settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + setSupportZoom(true) + builtInZoomControls = true + displayZoomControls = false + } + }, + client = remember(navigator) { + LoginWebViewClient( + navigator, + coroutineScope, + snackbarHostState + ) + }, + chromeClient = remember { MyWebChromeClient(context, coroutineScope) } + ) + + if (isLoading) { + LinearProgressIndicator( + progress = animatedProgress, + modifier = Modifier.fillMaxWidth() + ) + } + } + } +} + +class LoginWebViewClient( + nativeNavigator: DestinationsNavigator? = null, + val coroutineScope: CoroutineScope, + val snackbarHostState: SnackbarHostState, +) : MyWebViewClient(nativeNavigator) { + private var isLoadingAccount = false + + override fun injectCookies(url: String) {} + + override fun onPageFinished(view: WebView, url: String?) { + super.onPageFinished(view, url) + if (url == null) { + return + } + if (isLoadingAccount) { + return + } + val cookieStr = CookieManager.getInstance().getCookie(url) ?: return + val cookies = parseCookie(cookieStr).mapKeys { it.key.uppercase() } + val bduss = cookies["BDUSS"] + val sToken = cookies["STOKEN"] + val baiduId = cookies["BAIDUID"] + if (url.startsWith("https://tieba.baidu.com/index/tbwise/") || url.startsWith("https://tiebac.baidu.com/index/tbwise/")) { + if (bduss == null || sToken == null) { + return + } + if (!baiduId.isNullOrEmpty() && ClientUtils.baiduId.isNullOrEmpty()) { + coroutineScope.launch { + ClientUtils.saveBaiduId(context, baiduId) + } + } + coroutineScope.launch { + snackbarHostState.showSnackbar( + context.getString(R.string.text_please_wait), + duration = SnackbarDuration.Indefinite + ) + } + coroutineScope.launch { + AccountUtil.fetchAccountFlow(bduss, sToken, cookieStr) + .catch { + coroutineScope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + snackbarHostState.showSnackbar( + context.getString( + R.string.text_login_failed, + it.getErrorMessage() + ), duration = SnackbarDuration.Short + ) + } + navigator.loadUrl(LOGIN_URL) + isLoadingAccount = false + } + .flowOn(Dispatchers.Main) + .collect { account -> + isLoadingAccount = false + AccountUtil.newAccount(account.uid, account) { + if (it) { + AccountUtil.switchAccount(context, account.id) + coroutineScope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + snackbarHostState.showSnackbar( + context.getString(R.string.text_login_success), + duration = SnackbarDuration.Short + ) + } + coroutineScope.launch { + delay(1500L) + nativeNavigator?.navigateUp() + } + } else { + coroutineScope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + snackbarHostState.showSnackbar( + context.getString(R.string.text_login_failed_default), + duration = SnackbarDuration.Short + ) + } + view.loadUrl(LOGIN_URL) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt index aaa0cfbd..85b5dbff 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt @@ -64,16 +64,15 @@ import com.airbnb.lottie.compose.rememberLottieComposition import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.placeholder.placeholder import com.huanchengfly.tieba.post.R -import com.huanchengfly.tieba.post.activities.LoginActivity import com.huanchengfly.tieba.post.arch.GlobalEvent import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.onGlobalEvent import com.huanchengfly.tieba.post.arch.pageViewModel -import com.huanchengfly.tieba.post.goToActivity import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination +import com.huanchengfly.tieba.post.ui.page.destinations.LoginPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.SearchPageDestination import com.huanchengfly.tieba.post.ui.widgets.Chip import com.huanchengfly.tieba.post.ui.widgets.compose.ActionItem @@ -638,7 +637,7 @@ fun EmptyScreen( canOpenExplore: Boolean, onOpenExplore: () -> Unit ) { - val context = LocalContext.current + val navigator = LocalNavigator.current TipScreen( title = { if (!loggedIn) { @@ -671,7 +670,7 @@ fun EmptyScreen( if (!loggedIn) { Button( onClick = { - context.goToActivity() + navigator.navigate(LoginPageDestination) }, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/settings/SettingsPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/settings/SettingsPage.kt index 0ca573da..4d5f650b 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/settings/SettingsPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/settings/SettingsPage.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.AccountCircle @@ -20,11 +21,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.google.accompanist.insets.ui.Scaffold import com.huanchengfly.tieba.post.R -import com.huanchengfly.tieba.post.activities.LoginActivity import com.huanchengfly.tieba.post.dataStore -import com.huanchengfly.tieba.post.goToActivity import com.huanchengfly.tieba.post.models.database.Account import com.huanchengfly.tieba.post.ui.common.prefs.PrefsScreen import com.huanchengfly.tieba.post.ui.common.prefs.widgets.TextPref @@ -35,6 +33,7 @@ import com.huanchengfly.tieba.post.ui.page.destinations.AccountManagePageDestina import com.huanchengfly.tieba.post.ui.page.destinations.BlockSettingsPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.CustomSettingsPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.HabitSettingsPageDestination +import com.huanchengfly.tieba.post.ui.page.destinations.LoginPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.MoreSettingsPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.OKSignSettingsPageDestination import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar @@ -63,8 +62,8 @@ fun NowAccountItem( account: Account?, modifier: Modifier = Modifier ) { + val navigator = LocalNavigator.current if (account != null) { - val navigator = LocalNavigator.current TextPref( title = stringResource(id = R.string.title_account_manage), summary = stringResource(id = R.string.summary_now_account, account.nameShow ?: account.name), @@ -82,12 +81,11 @@ fun NowAccountItem( modifier = modifier, ) } else { - val context = LocalContext.current TextPref( title = stringResource(id = R.string.title_account_manage), summary = stringResource(id = R.string.summary_not_logged_in), enabled = true, - onClick = { context.goToActivity() }, + onClick = { navigator.navigate(LoginPageDestination) }, leadingIcon = { LeadingIcon { AvatarIcon( @@ -110,23 +108,23 @@ fun NowAccountItem( fun SettingsPage( navigator: DestinationsNavigator, ) { - Scaffold( - backgroundColor = Color.Transparent, - topBar = { - TitleCentredToolbar( - title = { - Text( - text = stringResource(id = R.string.title_settings), - fontWeight = FontWeight.Bold, style = MaterialTheme.typography.h6 - ) - }, - navigationIcon = { - BackNavigationIcon(onBackPressed = { navigator.navigateUp() }) - } - ) - }, - ) { - ProvideNavigator(navigator = navigator) { + ProvideNavigator(navigator = navigator) { + Scaffold( + backgroundColor = Color.Transparent, + topBar = { + TitleCentredToolbar( + title = { + Text( + text = stringResource(id = R.string.title_settings), + fontWeight = FontWeight.Bold, style = MaterialTheme.typography.h6 + ) + }, + navigationIcon = { + BackNavigationIcon(onBackPressed = { navigator.navigateUp() }) + } + ) + }, + ) { PrefsScreen( dataStore = LocalContext.current.dataStore, dividerThickness = 0.dp, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/settings/account/AccountManagePage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/settings/account/AccountManagePage.kt index c00e7f4b..2ab151e7 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/settings/account/AccountManagePage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/settings/account/AccountManagePage.kt @@ -31,14 +31,13 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.huanchengfly.tieba.post.R -import com.huanchengfly.tieba.post.activities.LoginActivity import com.huanchengfly.tieba.post.dataStore -import com.huanchengfly.tieba.post.goToActivity import com.huanchengfly.tieba.post.ui.common.prefs.PrefsScreen import com.huanchengfly.tieba.post.ui.common.prefs.widgets.DropDownPref import com.huanchengfly.tieba.post.ui.common.prefs.widgets.EditTextPref import com.huanchengfly.tieba.post.ui.common.prefs.widgets.TextPref import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme +import com.huanchengfly.tieba.post.ui.page.destinations.LoginPageDestination import com.huanchengfly.tieba.post.ui.page.settings.LeadingIcon import com.huanchengfly.tieba.post.ui.widgets.compose.AvatarIcon import com.huanchengfly.tieba.post.ui.widgets.compose.BackNavigationIcon @@ -130,7 +129,7 @@ fun AccountManagePage( prefsItem { TextPref( title = stringResource(id = R.string.title_new_account), - onClick = { context.goToActivity() }, + onClick = { navigator.navigate(LoginPageDestination) }, leadingIcon = { LeadingIcon { AvatarIcon( diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Toolbar.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Toolbar.kt index 4d23c10b..4e9f56ca 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Toolbar.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Toolbar.kt @@ -50,13 +50,13 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.huanchengfly.tieba.post.R -import com.huanchengfly.tieba.post.activities.LoginActivity import com.huanchengfly.tieba.post.arch.BaseComposeActivity.Companion.LocalWindowSizeClass import com.huanchengfly.tieba.post.arch.GlobalEvent import com.huanchengfly.tieba.post.arch.emitGlobalEvent -import com.huanchengfly.tieba.post.goToActivity import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass.Companion.Compact +import com.huanchengfly.tieba.post.ui.page.LocalNavigator +import com.huanchengfly.tieba.post.ui.page.destinations.LoginPageDestination import com.huanchengfly.tieba.post.utils.AccountUtil import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount import com.huanchengfly.tieba.post.utils.StringUtil @@ -73,6 +73,7 @@ fun AccountNavIcon( spacer: Boolean = true, size: Dp = Sizes.Small ) { + val navigator = LocalNavigator.current val currentAccount = LocalAccount.current if (spacer) Spacer(modifier = Modifier.width(12.dp)) if (currentAccount == null) { @@ -125,9 +126,11 @@ fun AccountNavIcon( VerticalDivider( modifier = Modifier.padding(vertical = 8.dp) ) - DropdownMenuItem(onClick = { - context.goToActivity() - }) { + DropdownMenuItem( + onClick = { + navigator.navigate(LoginPageDestination) + } + ) { Icon( imageVector = Icons.Rounded.Add, contentDescription = stringResource(id = R.string.title_new_account), diff --git a/app/src/main/java/com/huanchengfly/tieba/post/utils/AccountUtil.kt b/app/src/main/java/com/huanchengfly/tieba/post/utils/AccountUtil.kt index 972eeb96..1b3bf103 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/utils/AccountUtil.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/utils/AccountUtil.kt @@ -199,7 +199,7 @@ object AccountUtil { fun parseCookie(cookie: String): Map { return cookie .split(";") - .map { it.split("=") } + .map { it.trim().split("=") } .filter { it.size > 1 } .associate { it.first() to it.drop(1).joinToString("=") } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a07a6b9b..84dfa177 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -761,4 +761,7 @@ 添加到白名单 屏蔽选项 %s领域大神 + 登录失败 %s + 登录成功,即将跳转 + 登录失败