|
|
@@ -6,6 +6,7 @@ import android.annotation.SuppressLint
|
|
|
import android.net.Uri
|
|
|
import android.net.http.SslError
|
|
|
import android.os.SystemClock
|
|
|
+import android.view.MotionEvent
|
|
|
import android.view.View
|
|
|
import android.webkit.JavascriptInterface
|
|
|
import android.webkit.SslErrorHandler
|
|
|
@@ -18,11 +19,18 @@ import android.webkit.WebViewClient
|
|
|
import androidx.activity.compose.BackHandler
|
|
|
import androidx.activity.compose.LocalActivity
|
|
|
import androidx.appcompat.app.AlertDialog
|
|
|
+import androidx.compose.animation.animateContentSize
|
|
|
import androidx.compose.foundation.layout.Box
|
|
|
import androidx.compose.foundation.layout.Column
|
|
|
+import androidx.compose.foundation.layout.Row
|
|
|
+import androidx.compose.foundation.layout.Spacer
|
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
|
import androidx.compose.foundation.layout.padding
|
|
|
+import androidx.compose.foundation.layout.statusBarsPadding
|
|
|
+import androidx.compose.foundation.layout.width
|
|
|
+import androidx.compose.foundation.layout.widthIn
|
|
|
+import androidx.compose.foundation.clickable
|
|
|
import androidx.compose.material.icons.Icons
|
|
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
|
import androidx.compose.material.icons.automirrored.filled.Login
|
|
|
@@ -46,9 +54,11 @@ import androidx.compose.runtime.mutableStateOf
|
|
|
import androidx.compose.runtime.remember
|
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
|
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.res.stringResource
|
|
|
+import androidx.compose.ui.text.style.TextOverflow
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
import androidx.core.net.toUri
|
|
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
|
@@ -67,16 +77,18 @@ import io.legado.app.ui.config.otherConfig.OtherConfig
|
|
|
import io.legado.app.ui.login.SourceLoginActivity
|
|
|
import io.legado.app.ui.theme.LegadoTheme
|
|
|
import io.legado.app.ui.widget.components.AppTextField
|
|
|
+import io.legado.app.ui.widget.components.button.MediumIconButton
|
|
|
+import io.legado.app.ui.widget.components.card.GlassCard
|
|
|
+import io.legado.app.ui.widget.components.card.NormalCard
|
|
|
import io.legado.app.ui.widget.components.button.SmallIconButton
|
|
|
+import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
|
|
import io.legado.app.ui.widget.components.button.TopBarActionButton
|
|
|
-import io.legado.app.ui.widget.components.button.TopBarNavigationButton
|
|
|
-import io.legado.app.ui.widget.components.card.GlassCard
|
|
|
+import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
|
|
|
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
|
|
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
|
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
|
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
|
|
import io.legado.app.ui.widget.components.text.AppText
|
|
|
-import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
|
|
import io.legado.app.utils.NetworkUtils
|
|
|
import io.legado.app.utils.isTrue
|
|
|
import io.legado.app.utils.keepScreenOn
|
|
|
@@ -86,6 +98,8 @@ import io.legado.app.utils.setDarkeningAllowed
|
|
|
import io.legado.app.utils.share
|
|
|
import io.legado.app.utils.startActivity
|
|
|
import io.legado.app.utils.toastOnUi
|
|
|
+import kotlinx.coroutines.Job
|
|
|
+import kotlinx.coroutines.delay
|
|
|
import kotlinx.coroutines.launch
|
|
|
import org.apache.commons.text.StringEscapeUtils
|
|
|
import org.jsoup.Jsoup
|
|
|
@@ -115,6 +129,8 @@ fun RssReadRouteScreen(
|
|
|
var webProgress by remember { mutableIntStateOf(100) }
|
|
|
var showMenu by remember { mutableStateOf(false) }
|
|
|
var showRedirectMenu by remember { mutableStateOf(false) }
|
|
|
+ var showFloatingCard by remember { mutableStateOf(true) }
|
|
|
+ var showFloatingCardJob by remember { mutableStateOf<Job?>(null) }
|
|
|
|
|
|
var webView by remember { mutableStateOf<WebView?>(null) }
|
|
|
|
|
|
@@ -174,157 +190,208 @@ fun RssReadRouteScreen(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- Column(modifier = Modifier.fillMaxSize()) {
|
|
|
- GlassMediumFlexibleTopAppBar(
|
|
|
- title = pageTitle.ifBlank { defaultTopBarTitle },
|
|
|
- navigationIcon = {
|
|
|
- TopBarNavigationButton(
|
|
|
- onClick = onBackClick,
|
|
|
- imageVector = Icons.AutoMirrored.Filled.ArrowBack
|
|
|
- )
|
|
|
- },
|
|
|
- scrollBehavior = null,
|
|
|
- actions = {
|
|
|
- TopBarActionButton(
|
|
|
- onClick = { viewModel.refresh { webView?.reload() } },
|
|
|
- imageVector = Icons.Default.Refresh,
|
|
|
- contentDescription = stringResource(R.string.refresh)
|
|
|
- )
|
|
|
- TopBarActionButton(
|
|
|
- onClick = {
|
|
|
- viewModel.addFavorite()
|
|
|
- favoriteTitle = viewModel.rssArticle?.title.orEmpty()
|
|
|
- favoriteGroup = viewModel.rssArticle?.group.orEmpty()
|
|
|
- showFavoriteSheet = true
|
|
|
- },
|
|
|
- imageVector = Icons.Default.Star,
|
|
|
- contentDescription = stringResource(R.string.favorite)
|
|
|
- )
|
|
|
- TopBarActionButton(
|
|
|
- onClick = { showMenu = true },
|
|
|
- imageVector = Icons.Default.MoreVert,
|
|
|
- contentDescription = "Menu"
|
|
|
- )
|
|
|
- RoundDropdownMenu(
|
|
|
- expanded = showMenu,
|
|
|
- onDismissRequest = { showMenu = false }
|
|
|
- ) { dismiss ->
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.share),
|
|
|
- leadingIcon = { MenuItemIcon(Icons.Default.Share) },
|
|
|
- onClick = {
|
|
|
- dismiss()
|
|
|
- webView?.url?.let {
|
|
|
- context.share(it)
|
|
|
- } ?: viewModel.rssArticle?.let {
|
|
|
- context.share(it.link)
|
|
|
- } ?: context.toastOnUi(R.string.null_url)
|
|
|
+ Box(modifier = Modifier.fillMaxSize()) {
|
|
|
+ VisibleWebViewCompose(
|
|
|
+ modifier = Modifier.fillMaxSize(),
|
|
|
+ onCreated = { createdWebView ->
|
|
|
+ webView = createdWebView
|
|
|
+ configureRssReadWebView(
|
|
|
+ webView = createdWebView,
|
|
|
+ context = context,
|
|
|
+ activity = activity,
|
|
|
+ appCompatActivity = appCompatActivity,
|
|
|
+ viewModel = viewModel,
|
|
|
+ initialTitle = title,
|
|
|
+ redirectPolicyProvider = { redirectPolicy },
|
|
|
+ callbacks = RssReadWebControllerCallbacks(
|
|
|
+ onProgressChanged = { webProgress = it },
|
|
|
+ onPageTitleResolved = { resolved ->
|
|
|
+ pageTitle = resolved.ifBlank { defaultTopBarTitle }
|
|
|
}
|
|
|
)
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = if (isSpeaking) stringResource(R.string.aloud_stop) else stringResource(
|
|
|
- R.string.read_aloud
|
|
|
- ),
|
|
|
- leadingIcon = {
|
|
|
- MenuItemIcon(if (isSpeaking) Icons.Default.Stop else Icons.Default.VolumeUp)
|
|
|
- },
|
|
|
- onClick = {
|
|
|
- dismiss()
|
|
|
- if (isSpeaking) {
|
|
|
- viewModel.stopReadAloud()
|
|
|
- } else {
|
|
|
- webView?.evaluateJavascript("document.documentElement.outerHTML") {
|
|
|
- val html = StringEscapeUtils.unescapeJson(it)
|
|
|
- .replace("^\"|\"$".toRegex(), "")
|
|
|
- viewModel.readAloud(
|
|
|
- Jsoup.parse(html)
|
|
|
- .text()
|
|
|
- )
|
|
|
- }
|
|
|
+ )
|
|
|
+ createdWebView.setOnTouchListener { _, event ->
|
|
|
+ when (event.actionMasked) {
|
|
|
+ MotionEvent.ACTION_DOWN,
|
|
|
+ MotionEvent.ACTION_MOVE -> {
|
|
|
+ showFloatingCardJob?.cancel()
|
|
|
+ showFloatingCard = false
|
|
|
+ }
|
|
|
+
|
|
|
+ MotionEvent.ACTION_UP,
|
|
|
+ MotionEvent.ACTION_CANCEL -> {
|
|
|
+ createdWebView.performClick()
|
|
|
+ showFloatingCardJob?.cancel()
|
|
|
+ showFloatingCardJob = scope.launch {
|
|
|
+ delay(180L)
|
|
|
+ showFloatingCard = true
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
+ false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ )
|
|
|
+ if (webProgress in 0..99) {
|
|
|
+ LinearProgressIndicator(
|
|
|
+ progress = { webProgress / 100f },
|
|
|
+ modifier = Modifier.fillMaxWidth()
|
|
|
+ )
|
|
|
+ }
|
|
|
+ TopFloatingStickyItem(
|
|
|
+ item = if (showFloatingCard) {
|
|
|
+ RssReadFloatingSummary(
|
|
|
+ titleText = pageTitle.ifBlank { defaultTopBarTitle },
|
|
|
+ progressText = if (webProgress in 0..99) " · ${webProgress}%" else null
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ null
|
|
|
+ },
|
|
|
+ modifier = Modifier
|
|
|
+ .align(Alignment.TopCenter)
|
|
|
+ .statusBarsPadding()
|
|
|
+ .padding(top = 6.dp),
|
|
|
+ ) { summary ->
|
|
|
+ NormalCard(
|
|
|
+ modifier = Modifier.animateContentSize(),
|
|
|
+ cornerRadius = 32.dp,
|
|
|
+ containerColor = LegadoTheme.colorScheme.surfaceContainer,
|
|
|
+ contentColor = LegadoTheme.colorScheme.onCardContainer
|
|
|
+ ) {
|
|
|
+ Row(
|
|
|
+ modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
|
|
+ verticalAlignment = Alignment.CenterVertically
|
|
|
+ ) {
|
|
|
+ SmallTonalIconButton(
|
|
|
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
|
|
+ contentDescription = stringResource(R.string.back),
|
|
|
+ onClick = onBackClick
|
|
|
)
|
|
|
- if (!viewModel.rssSource?.loginUrl.isNullOrBlank()) {
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.login),
|
|
|
- leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
|
|
|
- onClick = {
|
|
|
- dismiss()
|
|
|
- context.startActivity<SourceLoginActivity> {
|
|
|
- putExtra("type", "rssSource")
|
|
|
- putExtra("key", viewModel.rssSource?.sourceUrl)
|
|
|
- }
|
|
|
- }
|
|
|
- )
|
|
|
+ Spacer(modifier = Modifier.width(8.dp))
|
|
|
+ AppText(
|
|
|
+ text = summary.titleText,
|
|
|
+ style = LegadoTheme.typography.labelSmallEmphasized,
|
|
|
+ maxLines = 1,
|
|
|
+ overflow = TextOverflow.Ellipsis,
|
|
|
+ modifier = Modifier.padding(horizontal = 12.dp)
|
|
|
+ )
|
|
|
+ summary.progressText?.let { text ->
|
|
|
+ AppText(text = text, style = LegadoTheme.typography.labelMediumEmphasized)
|
|
|
+ Spacer(modifier = Modifier.width(6.dp))
|
|
|
}
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.redirect_policy),
|
|
|
- onClick = { showRedirectMenu = true }
|
|
|
+ SmallTonalIconButton(
|
|
|
+ imageVector = Icons.Default.Refresh,
|
|
|
+ contentDescription = stringResource(R.string.refresh),
|
|
|
+ onClick = { viewModel.refresh { webView?.reload() } }
|
|
|
)
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.open_in_browser),
|
|
|
- leadingIcon = { MenuItemIcon(Icons.Default.OpenInBrowser) },
|
|
|
+ Spacer(modifier = Modifier.width(8.dp))
|
|
|
+ SmallTonalIconButton(
|
|
|
+ imageVector = Icons.Default.Star,
|
|
|
+ contentDescription = stringResource(R.string.favorite),
|
|
|
onClick = {
|
|
|
- dismiss()
|
|
|
- webView?.url?.let { context.openUrl(it) } ?: context.toastOnUi("url null")
|
|
|
+ viewModel.addFavorite()
|
|
|
+ favoriteTitle = viewModel.rssArticle?.title.orEmpty()
|
|
|
+ favoriteGroup = viewModel.rssArticle?.group.orEmpty()
|
|
|
+ showFavoriteSheet = true
|
|
|
}
|
|
|
)
|
|
|
- }
|
|
|
-
|
|
|
- RoundDropdownMenu(
|
|
|
- expanded = showRedirectMenu,
|
|
|
- onDismissRequest = { showRedirectMenu = false }
|
|
|
- ) { dismiss ->
|
|
|
- RedirectPolicy.entries.forEach { policy ->
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = policy.title(),
|
|
|
- onClick = {
|
|
|
- dismiss()
|
|
|
- showMenu = false
|
|
|
- viewModel.rssSource?.let { source ->
|
|
|
- viewModel.updateRssSourceRedirectPolicy(source.sourceUrl, policy.name)
|
|
|
- redirectPolicy = policy
|
|
|
+ Spacer(modifier = Modifier.width(8.dp))
|
|
|
+ Box {
|
|
|
+ SmallTonalIconButton(
|
|
|
+ imageVector = Icons.Default.MoreVert,
|
|
|
+ contentDescription = "Menu",
|
|
|
+ onClick = { showMenu = true }
|
|
|
+ )
|
|
|
+ RoundDropdownMenu(
|
|
|
+ expanded = showMenu,
|
|
|
+ onDismissRequest = { showMenu = false }
|
|
|
+ ) { dismiss ->
|
|
|
+ RoundDropdownMenuItem(
|
|
|
+ text = stringResource(R.string.share),
|
|
|
+ leadingIcon = { MenuItemIcon(Icons.Default.Share) },
|
|
|
+ onClick = {
|
|
|
+ dismiss()
|
|
|
+ webView?.url?.let {
|
|
|
+ context.share(it)
|
|
|
+ } ?: viewModel.rssArticle?.let {
|
|
|
+ context.share(it.link)
|
|
|
+ } ?: context.toastOnUi(R.string.null_url)
|
|
|
}
|
|
|
- context.toastOnUi("重定向策略已更新")
|
|
|
- },
|
|
|
- trailingIcon = {
|
|
|
- if (policy == redirectPolicy) {
|
|
|
- Icon(Icons.Default.Check, null)
|
|
|
+ )
|
|
|
+ RoundDropdownMenuItem(
|
|
|
+ text = if (isSpeaking) stringResource(R.string.aloud_stop) else stringResource(
|
|
|
+ R.string.read_aloud
|
|
|
+ ),
|
|
|
+ leadingIcon = {
|
|
|
+ MenuItemIcon(if (isSpeaking) Icons.Default.Stop else Icons.Default.VolumeUp)
|
|
|
+ },
|
|
|
+ onClick = {
|
|
|
+ dismiss()
|
|
|
+ if (isSpeaking) {
|
|
|
+ viewModel.stopReadAloud()
|
|
|
+ } else {
|
|
|
+ webView?.evaluateJavascript("document.documentElement.outerHTML") {
|
|
|
+ val html = StringEscapeUtils.unescapeJson(it)
|
|
|
+ .replace("^\"|\"$".toRegex(), "")
|
|
|
+ viewModel.readAloud(
|
|
|
+ Jsoup.parse(html).text()
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+ )
|
|
|
+ if (!viewModel.rssSource?.loginUrl.isNullOrBlank()) {
|
|
|
+ RoundDropdownMenuItem(
|
|
|
+ text = stringResource(R.string.login),
|
|
|
+ leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
|
|
|
+ onClick = {
|
|
|
+ dismiss()
|
|
|
+ context.startActivity<SourceLoginActivity> {
|
|
|
+ putExtra("type", "rssSource")
|
|
|
+ putExtra("key", viewModel.rssSource?.sourceUrl)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ )
|
|
|
}
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- )
|
|
|
-
|
|
|
- Box(modifier = Modifier.fillMaxSize()) {
|
|
|
- VisibleWebViewCompose(
|
|
|
- modifier = Modifier.fillMaxSize(),
|
|
|
- onCreated = { createdWebView ->
|
|
|
- webView = createdWebView
|
|
|
- configureRssReadWebView(
|
|
|
- webView = createdWebView,
|
|
|
- context = context,
|
|
|
- activity = activity,
|
|
|
- appCompatActivity = appCompatActivity,
|
|
|
- viewModel = viewModel,
|
|
|
- initialTitle = title,
|
|
|
- redirectPolicyProvider = { redirectPolicy },
|
|
|
- callbacks = RssReadWebControllerCallbacks(
|
|
|
- onProgressChanged = { webProgress = it },
|
|
|
- onPageTitleResolved = { resolved ->
|
|
|
- pageTitle = resolved.ifBlank { defaultTopBarTitle }
|
|
|
+ RoundDropdownMenuItem(
|
|
|
+ text = stringResource(R.string.redirect_policy),
|
|
|
+ onClick = { showRedirectMenu = true }
|
|
|
+ )
|
|
|
+ RoundDropdownMenuItem(
|
|
|
+ text = stringResource(R.string.open_in_browser),
|
|
|
+ leadingIcon = { MenuItemIcon(Icons.Default.OpenInBrowser) },
|
|
|
+ onClick = {
|
|
|
+ dismiss()
|
|
|
+ webView?.url?.let { context.openUrl(it) } ?: context.toastOnUi("url null")
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+ RoundDropdownMenu(
|
|
|
+ expanded = showRedirectMenu,
|
|
|
+ onDismissRequest = { showRedirectMenu = false }
|
|
|
+ ) { dismiss ->
|
|
|
+ RedirectPolicy.entries.forEach { policy ->
|
|
|
+ RoundDropdownMenuItem(
|
|
|
+ text = policy.title(),
|
|
|
+ onClick = {
|
|
|
+ dismiss()
|
|
|
+ showMenu = false
|
|
|
+ viewModel.rssSource?.let { source ->
|
|
|
+ viewModel.updateRssSourceRedirectPolicy(source.sourceUrl, policy.name)
|
|
|
+ redirectPolicy = policy
|
|
|
+ }
|
|
|
+ context.toastOnUi("重定向策略已更新")
|
|
|
+ },
|
|
|
+ trailingIcon = {
|
|
|
+ if (policy == redirectPolicy) {
|
|
|
+ Icon(Icons.Default.Check, null)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ )
|
|
|
}
|
|
|
- )
|
|
|
- )
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
- )
|
|
|
- if (webProgress in 0..99) {
|
|
|
- LinearProgressIndicator(
|
|
|
- progress = { webProgress / 100f },
|
|
|
- modifier = Modifier.fillMaxWidth()
|
|
|
- )
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -347,6 +414,11 @@ fun RssReadRouteScreen(
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+private data class RssReadFloatingSummary(
|
|
|
+ val titleText: String,
|
|
|
+ val progressText: String?,
|
|
|
+)
|
|
|
+
|
|
|
@Composable
|
|
|
private fun FavoriteEditSheet(
|
|
|
show: Boolean,
|