[优化] 修复订阅源视频播放问题和新增源编辑
This commit is contained in:
@@ -301,4 +301,15 @@ abstract class BaseRuleViewModel<T : SelectableItem<ID>, Entity, ID, S : ListUiS
|
||||
_importState.value = currentState.copy(items = newItems)
|
||||
}
|
||||
|
||||
}
|
||||
fun updateImportItem(index: Int, data: Entity) {
|
||||
val currentState = _importState.value as? BaseImportUiState.Success<Entity> ?: return
|
||||
if (index !in currentState.items.indices) return
|
||||
val newItems = currentState.items.toMutableList()
|
||||
newItems[index] = newItems[index].copy(data = data)
|
||||
_importState.value = currentState.copy(
|
||||
items = newItems,
|
||||
version = currentState.version + 1
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -191,6 +191,7 @@ fun TxtRuleScreen(
|
||||
onDismissRequest = { viewModel.cancelImport() },
|
||||
onToggleItem = { viewModel.toggleImportSelection(it) },
|
||||
onToggleAll = { viewModel.toggleImportAll(it) },
|
||||
onUpdateItem = { index, rule -> viewModel.updateImportItem(index, rule) },
|
||||
onConfirm = { viewModel.saveImportedRules() },
|
||||
itemTitle = { rule -> rule.name },
|
||||
itemSubtitle = { rule ->
|
||||
|
||||
@@ -182,6 +182,7 @@ fun DictRuleScreen(
|
||||
onDismissRequest = { viewModel.cancelImport() },
|
||||
onToggleItem = { viewModel.toggleImportSelection(it) },
|
||||
onToggleAll = { viewModel.toggleImportAll(it) },
|
||||
onUpdateItem = { index, rule -> viewModel.updateImportItem(index, rule) },
|
||||
onConfirm = { viewModel.saveImportedRules() },
|
||||
itemTitle = { rule -> rule.name },
|
||||
itemSubtitle = { rule ->
|
||||
|
||||
@@ -188,6 +188,7 @@ fun ReplaceRuleScreen(
|
||||
onDismissRequest = { viewModel.cancelImport() },
|
||||
onToggleItem = { viewModel.toggleImportSelection(it) },
|
||||
onToggleAll = { viewModel.toggleImportAll(it) },
|
||||
onUpdateItem = { index, rule -> viewModel.updateImportItem(index, rule) },
|
||||
onConfirm = { viewModel.saveImportedRules() },
|
||||
topBarActions = {},
|
||||
itemTitle = { rule -> rule.name },
|
||||
|
||||
@@ -59,6 +59,7 @@ import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.RssArticle
|
||||
import io.legado.app.data.entities.RssSource
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.cover.buildCoverImageRequest
|
||||
@@ -92,6 +93,7 @@ fun RssArticlesPage(
|
||||
rssSource: RssSource?,
|
||||
viewModel: RssArticlesViewModel,
|
||||
onRead: (RssArticle) -> Unit,
|
||||
paddingValues: PaddingValues = PaddingValues(0.dp),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -123,6 +125,10 @@ fun RssArticlesPage(
|
||||
}
|
||||
|
||||
val refreshState = rememberPullToRefreshState()
|
||||
val contentPadding = adaptiveContentPadding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = 120.dp
|
||||
)
|
||||
|
||||
PullToRefreshBox(
|
||||
isRefreshing = loadState.isRefreshing,
|
||||
@@ -141,7 +147,7 @@ fun RssArticlesPage(
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(start = 8.dp, end = 8.dp, bottom = 72.dp),
|
||||
contentPadding = contentPadding,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(
|
||||
@@ -174,7 +180,7 @@ fun RssArticlesPage(
|
||||
columns = GridCells.Fixed(2),
|
||||
state = gridState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(start = 8.dp, end = 8.dp, bottom = 72.dp),
|
||||
contentPadding = contentPadding,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
@@ -208,7 +214,7 @@ fun RssArticlesPage(
|
||||
columns = StaggeredGridCells.Fixed(2),
|
||||
state = staggeredState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(start = 8.dp, end = 8.dp, bottom = 72.dp),
|
||||
contentPadding = contentPadding,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalItemSpacing = 8.dp
|
||||
) {
|
||||
|
||||
@@ -156,7 +156,7 @@ fun RssSortRouteScreen(
|
||||
}
|
||||
context.toastOnUi("重定向策略已更新")
|
||||
},
|
||||
pagerContent = { _, sort ->
|
||||
pagerContent = { _, sort, paddingValues ->
|
||||
val pageViewModel: RssArticlesViewModel = koinViewModel(
|
||||
key = "rss_${viewModel.url}_${sort.first}_${sort.second}"
|
||||
)
|
||||
@@ -167,6 +167,7 @@ fun RssSortRouteScreen(
|
||||
rssUrl = viewModel.url,
|
||||
rssSource = viewModel.rssSource,
|
||||
viewModel = pageViewModel,
|
||||
paddingValues = paddingValues,
|
||||
onRead = { article ->
|
||||
viewModel.read(article)
|
||||
onOpenRead(article.title, article.origin, article.link, null)
|
||||
|
||||
@@ -89,7 +89,7 @@ fun RssSortScreen(
|
||||
onOpenReadRecord: (RssReadRecord) -> Unit,
|
||||
onClearArticles: () -> Unit,
|
||||
onRedirectPolicyChanged: (RedirectPolicy) -> Unit,
|
||||
pagerContent: @Composable (index: Int, item: Pair<String, String>) -> Unit
|
||||
pagerContent: @Composable (index: Int, item: Pair<String, String>, paddingValues: PaddingValues) -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||
@@ -291,12 +291,10 @@ fun RssSortScreen(
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
contentPadding = PaddingValues(bottom = 0.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) { page ->
|
||||
val item = sortList.getOrNull(page) ?: return@HorizontalPager
|
||||
pagerContent(page, item)
|
||||
pagerContent(page, item, paddingValues)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,4 +368,3 @@ private fun RssReadRecordSheet(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
||||
import android.net.http.SslError
|
||||
import android.os.SystemClock
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.URLUtil
|
||||
@@ -19,10 +20,13 @@ import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -43,17 +47,22 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -61,6 +70,7 @@ import com.script.rhino.runScriptWithContext
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.http.CookieManager
|
||||
import io.legado.app.lib.dialogs.SelectItem
|
||||
import io.legado.app.lib.dialogs.selector
|
||||
@@ -69,6 +79,7 @@ import io.legado.app.ui.association.OnLineImportActivity
|
||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||
import io.legado.app.ui.login.SourceLoginActivity
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
|
||||
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.button.SmallIconButton
|
||||
@@ -87,6 +98,7 @@ 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 io.legado.app.utils.toggleSystemBar
|
||||
import org.apache.commons.text.StringEscapeUtils
|
||||
import org.jsoup.Jsoup
|
||||
import java.net.URLDecoder
|
||||
@@ -115,6 +127,10 @@ fun RssReadRouteScreen(
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
var showRedirectMenu by remember { mutableStateOf(false) }
|
||||
var webView by remember { mutableStateOf<WebView?>(null) }
|
||||
var customView by remember { mutableStateOf<View?>(null) }
|
||||
var customViewCallback by remember {
|
||||
mutableStateOf<WebChromeClient.CustomViewCallback?>(null)
|
||||
}
|
||||
|
||||
var redirectPolicy by remember { mutableStateOf(RedirectPolicy.ALLOW_ALL) }
|
||||
|
||||
@@ -128,6 +144,15 @@ fun RssReadRouteScreen(
|
||||
val isSpeaking by viewModel.isSpeakingState.collectAsStateWithLifecycle()
|
||||
val fallbackUserAgent = OtherConfig.userAgent
|
||||
|
||||
fun hideCustomView() {
|
||||
val currentCustomView = customView ?: return
|
||||
(currentCustomView.parent as? ViewGroup)?.removeView(currentCustomView)
|
||||
customView = null
|
||||
customViewCallback = null
|
||||
activity?.keepScreenOn(false)
|
||||
activity?.toggleSystemBar(AppConfig.showStatusBar)
|
||||
}
|
||||
|
||||
LaunchedEffect(origin, link, openUrl, title) {
|
||||
viewModel.initData(
|
||||
ReadRssArgs(
|
||||
@@ -167,169 +192,207 @@ fun RssReadRouteScreen(
|
||||
|
||||
BackHandler {
|
||||
when {
|
||||
customView != null -> customViewCallback?.onCustomViewHidden() ?: hideCustomView()
|
||||
webView?.canGoBack() == true && (webView?.copyBackForwardList()?.size ?: 0) > 1 -> webView?.goBack()
|
||||
else -> onBackClick()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = pageTitle.ifBlank { defaultTopBarTitle },
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
TopBarNavigationButton(onClick = onBackClick)
|
||||
},
|
||||
actions = {
|
||||
TopBarActionButton(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = stringResource(R.string.refresh),
|
||||
onClick = { viewModel.refresh { webView?.reload() } }
|
||||
)
|
||||
TopBarActionButton(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = stringResource(R.string.favorite),
|
||||
onClick = {
|
||||
viewModel.addFavorite()
|
||||
favoriteTitle = viewModel.rssArticle?.title.orEmpty()
|
||||
favoriteGroup = viewModel.rssArticle?.group.orEmpty()
|
||||
showFavoriteSheet = true
|
||||
}
|
||||
)
|
||||
Box {
|
||||
TopBarActionButton(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
onClick = { showMenu = true }
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
customViewCallback?.onCustomViewHidden()
|
||||
hideCustomView()
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = pageTitle.ifBlank { defaultTopBarTitle },
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
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)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
TopBarNavigationButton(onClick = onBackClick)
|
||||
},
|
||||
actions = {
|
||||
TopBarActionButton(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = stringResource(R.string.refresh),
|
||||
onClick = { viewModel.refresh { webView?.reload() } }
|
||||
)
|
||||
TopBarActionButton(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = stringResource(R.string.favorite),
|
||||
onClick = {
|
||||
viewModel.addFavorite()
|
||||
favoriteTitle = viewModel.rssArticle?.title.orEmpty()
|
||||
favoriteGroup = viewModel.rssArticle?.group.orEmpty()
|
||||
showFavoriteSheet = true
|
||||
}
|
||||
)
|
||||
Box {
|
||||
TopBarActionButton(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
onClick = { showMenu = true }
|
||||
)
|
||||
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()) {
|
||||
RoundDropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) { dismiss ->
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.login),
|
||||
leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
|
||||
text = stringResource(R.string.share),
|
||||
leadingIcon = { MenuItemIcon(Icons.Default.Share) },
|
||||
onClick = {
|
||||
dismiss()
|
||||
context.startActivity<SourceLoginActivity> {
|
||||
putExtra("type", "rssSource")
|
||||
putExtra("key", viewModel.rssSource?.sourceUrl)
|
||||
webView?.url?.let {
|
||||
context.share(it)
|
||||
} ?: viewModel.rssArticle?.let {
|
||||
context.share(it.link)
|
||||
} ?: context.toastOnUi(R.string.null_url)
|
||||
}
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
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")
|
||||
}
|
||||
)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
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 }
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
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 }
|
||||
},
|
||||
onShowCustomView = { view, callback ->
|
||||
if (view == null) {
|
||||
callback?.onCustomViewHidden()
|
||||
} else if (customView != null) {
|
||||
callback?.onCustomViewHidden()
|
||||
} else {
|
||||
customView = view
|
||||
customViewCallback = callback
|
||||
activity?.keepScreenOn(true)
|
||||
activity?.toggleSystemBar(false)
|
||||
}
|
||||
},
|
||||
onHideCustomView = { hideCustomView() }
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
if (webProgress in 0..99) {
|
||||
LinearProgressIndicator(
|
||||
progress = { webProgress / 100f },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
)
|
||||
if (webProgress in 0..99) {
|
||||
LinearProgressIndicator(
|
||||
progress = { webProgress / 100f },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
}
|
||||
}
|
||||
|
||||
customView?.let { view ->
|
||||
key(view) {
|
||||
AndroidView(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.zIndex(1f),
|
||||
factory = {
|
||||
(view.parent as? ViewGroup)?.removeView(view)
|
||||
view
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -377,7 +440,7 @@ private fun FavoriteEditSheet(
|
||||
}
|
||||
) {
|
||||
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Column {
|
||||
AppTextField(
|
||||
value = titleValue,
|
||||
onValueChange = onTitleChange,
|
||||
@@ -396,11 +459,14 @@ private fun FavoriteEditSheet(
|
||||
)
|
||||
}
|
||||
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.action_save),
|
||||
leadingIcon = { MenuItemIcon(Icons.Default.Check) },
|
||||
onClick = onSave,
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
ConfirmDismissButtonsRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onDismiss = onDismissRequest,
|
||||
onConfirm = onSave,
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
confirmText = stringResource(R.string.action_save)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package io.legado.app.ui.rss.read
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.net.http.SslError
|
||||
import android.os.SystemClock
|
||||
@@ -19,10 +20,12 @@ import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.script.rhino.runScriptWithContext
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.lib.dialogs.SelectItem
|
||||
@@ -39,7 +42,9 @@ import java.net.URLDecoder
|
||||
|
||||
internal data class RssReadWebControllerCallbacks(
|
||||
val onProgressChanged: (Int) -> Unit,
|
||||
val onPageTitleResolved: (String) -> Unit
|
||||
val onPageTitleResolved: (String) -> Unit,
|
||||
val onShowCustomView: (View?, WebChromeClient.CustomViewCallback?) -> Unit,
|
||||
val onHideCustomView: () -> Unit
|
||||
)
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "JavascriptInterface")
|
||||
@@ -54,9 +59,21 @@ internal fun configureRssReadWebView(
|
||||
callbacks: RssReadWebControllerCallbacks
|
||||
) {
|
||||
webView.webChromeClient = object : WebChromeClient() {
|
||||
override fun getDefaultVideoPoster(): Bitmap {
|
||||
return super.getDefaultVideoPoster() ?: createBitmap(100, 100)
|
||||
}
|
||||
|
||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||
callbacks.onProgressChanged(newProgress)
|
||||
}
|
||||
|
||||
override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
|
||||
callbacks.onShowCustomView(view, callback)
|
||||
}
|
||||
|
||||
override fun onHideCustomView() {
|
||||
callbacks.onHideCustomView()
|
||||
}
|
||||
}
|
||||
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
@@ -218,13 +235,17 @@ internal fun configureRssReadWebView(
|
||||
webView.settings.apply {
|
||||
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||
domStorageEnabled = true
|
||||
mediaPlaybackRequiresUserGesture = false
|
||||
allowContentAccess = true
|
||||
builtInZoomControls = true
|
||||
displayZoomControls = false
|
||||
textZoom = 100
|
||||
setDarkeningAllowed(AppConfig.isNightTheme)
|
||||
if (viewModel.rssSource?.enableJs == true) {
|
||||
javaScriptEnabled = true
|
||||
userAgentString = viewModel.headerMap[AppConst.UA_NAME] ?: AppConfig.userAgent
|
||||
viewModel.rssSource?.let { source ->
|
||||
javaScriptEnabled = source.enableJs
|
||||
}
|
||||
cacheMode = WebSettings.LOAD_DEFAULT
|
||||
}
|
||||
|
||||
webView.addJavascriptInterface(object {
|
||||
|
||||
@@ -6,8 +6,10 @@ import android.util.AttributeSet
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
@@ -144,19 +146,31 @@ fun VisibleWebViewCompose(
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { context ->
|
||||
VisibleWebView(context).also {
|
||||
webViewHolder.webView = it
|
||||
onCreated(it)
|
||||
FrameLayout(context).apply {
|
||||
clipChildren = false
|
||||
clipToPadding = false
|
||||
val webView = VisibleWebView(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
addView(webView)
|
||||
webViewHolder.webView = webView
|
||||
onCreated(webView)
|
||||
}
|
||||
},
|
||||
update = {
|
||||
webViewHolder.webView = it
|
||||
update = { container ->
|
||||
webViewHolder.webView = container.getChildAt(0) as? VisibleWebView
|
||||
}
|
||||
)
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
onDestroyed?.invoke()
|
||||
webViewHolder.webView?.destroy()
|
||||
webViewHolder.webView?.let { webView ->
|
||||
(webView.parent as? ViewGroup)?.removeView(webView)
|
||||
webView.destroy()
|
||||
}
|
||||
webViewHolder.webView = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseRuleEvent
|
||||
import io.legado.app.data.entities.RssSource
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.widget.components.ActionItem
|
||||
import io.legado.app.ui.widget.components.DraggableSelectionHandler
|
||||
import io.legado.app.ui.widget.components.GroupManageBottomSheet
|
||||
@@ -55,7 +56,6 @@ import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
||||
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.rules.RuleListScaffold
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@@ -200,6 +200,7 @@ fun RssSourceScreen(
|
||||
onDismissRequest = { viewModel.cancelImport() },
|
||||
onToggleItem = { viewModel.toggleImportSelection(it) },
|
||||
onToggleAll = { viewModel.toggleImportAll(it) },
|
||||
onUpdateItem = { index, source -> viewModel.updateImportItem(index, source) },
|
||||
onConfirm = { viewModel.saveImportedRules() },
|
||||
itemTitle = { rule -> rule.sourceName },
|
||||
itemSubtitle = { rule ->
|
||||
@@ -344,9 +345,9 @@ fun RssSourceScreen(
|
||||
FastScrollLazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(
|
||||
top = paddingValues.calculateTopPadding() + 8.dp,
|
||||
bottom = paddingValues.calculateBottomPadding() + 80.dp
|
||||
contentPadding = adaptiveContentPadding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding() + 120.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
|
||||
+221
-112
@@ -2,6 +2,7 @@ package io.legado.app.ui.widget.components.importComponents
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -17,43 +18,38 @@ import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedToggleButton
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
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.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonNull
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.gson.JsonPrimitive
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.text.AnimatedText
|
||||
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import kotlinx.coroutines.launch
|
||||
import io.legado.app.utils.GSON
|
||||
|
||||
@Composable
|
||||
fun SourceInputDialog(
|
||||
@@ -117,6 +113,7 @@ fun <T> BatchImportDialog(
|
||||
onToggleItem: (index: Int) -> Unit,
|
||||
onToggleAll: (isSelected: Boolean) -> Unit,
|
||||
onItemInfoClick: (index: Int) -> Unit = {},
|
||||
onUpdateItem: (index: Int, data: T) -> Unit = { _, _ -> },
|
||||
topBarActions: @Composable RowScope.() -> Unit = {},
|
||||
itemTitle: (data: T) -> String,
|
||||
itemSubtitle: (data: T) -> String? = { null }
|
||||
@@ -130,81 +127,187 @@ fun <T> BatchImportDialog(
|
||||
|
||||
if (!show && cachedState == null) return
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val currentState = cachedState!!
|
||||
var editingIndex by remember(currentState.source) { mutableStateOf<Int?>(null) }
|
||||
val editingItem = editingIndex?.let { currentState.items.getOrNull(it) }
|
||||
val isEditing = editingItem != null
|
||||
val selectedCount = currentState.items.count { it.isSelected }
|
||||
val totalCount = currentState.items.size
|
||||
val allSelected = selectedCount == totalCount
|
||||
val sheetTitle = when {
|
||||
isEditing -> itemTitle(editingItem.data)
|
||||
selectedCount > 0 -> {
|
||||
stringResource(
|
||||
R.string.select_count,
|
||||
selectedCount,
|
||||
totalCount
|
||||
)
|
||||
}
|
||||
|
||||
else -> title
|
||||
}
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest
|
||||
) {
|
||||
AppScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
AnimatedText(
|
||||
if (selectedCount > 0)
|
||||
stringResource(
|
||||
R.string.select_count,
|
||||
selectedCount,
|
||||
totalCount
|
||||
)
|
||||
else
|
||||
title
|
||||
)
|
||||
},
|
||||
actions = topBarActions,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
bottomBar = {
|
||||
ImportBottomBar(
|
||||
selectedCount = selectedCount,
|
||||
totalCount = totalCount,
|
||||
onToggleSelectAll = onToggleAll,
|
||||
onConfirm = {
|
||||
val selectedData =
|
||||
currentState.items.filter { it.isSelected }.map { it.data }
|
||||
onConfirm(selectedData)
|
||||
},
|
||||
onCancel = onDismissRequest
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = Modifier.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
|
||||
title = sheetTitle,
|
||||
startAction = if (isEditing) {
|
||||
{
|
||||
SmallIconButton(
|
||||
onClick = { editingIndex = null },
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "返回"
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
itemsIndexed(
|
||||
currentState.items,
|
||||
key = { _, item -> item.data.hashCode() }) { index, itemWrapper ->
|
||||
ImportItemRow(
|
||||
title = itemTitle(itemWrapper.data),
|
||||
subtitle = itemSubtitle(itemWrapper.data),
|
||||
isSelected = itemWrapper.isSelected,
|
||||
status = itemWrapper.status,
|
||||
onClick = { onToggleItem(index) },
|
||||
onInfoClick = {
|
||||
scope.launch { snackbarHostState.showSnackbar("其实还没写桀桀桀") }
|
||||
onItemInfoClick(index)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
endAction = if (!isEditing) {
|
||||
{
|
||||
Row {
|
||||
topBarActions()
|
||||
SmallIconButton(
|
||||
onClick = { onToggleAll(!allSelected) },
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = if (allSelected) "全不选" else "全选"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
) {
|
||||
if (isEditing) {
|
||||
BatchImportJsonEditContent(
|
||||
data = editingItem.data,
|
||||
version = currentState.version
|
||||
) { data ->
|
||||
editingIndex?.let { onUpdateItem(it, data) }
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.58f)
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
itemsIndexed(
|
||||
currentState.items,
|
||||
key = { _, item -> item.data.hashCode() }
|
||||
) { index, itemWrapper ->
|
||||
ImportItemRow(
|
||||
title = itemTitle(itemWrapper.data),
|
||||
subtitle = itemSubtitle(itemWrapper.data),
|
||||
isSelected = itemWrapper.isSelected,
|
||||
status = itemWrapper.status,
|
||||
onClick = { onToggleItem(index) },
|
||||
onInfoClick = {
|
||||
onItemInfoClick(index)
|
||||
editingIndex = index
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmDismissButtonsRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(top = 8.dp, bottom = 8.dp),
|
||||
onDismiss = onDismissRequest,
|
||||
onConfirm = {
|
||||
val selectedData = currentState.items.filter { it.isSelected }.map { it.data }
|
||||
onConfirm(selectedData)
|
||||
},
|
||||
dismissText = "取消",
|
||||
confirmText = "导入",
|
||||
confirmEnabled = selectedCount > 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ConfigurationScreenWidthHeight")
|
||||
@Composable
|
||||
private fun <T> BatchImportJsonEditContent(
|
||||
data: T,
|
||||
version: Int,
|
||||
onDataChange: (T) -> Unit
|
||||
) {
|
||||
val jsonObject = remember(version) { data.toImportJsonObject() }
|
||||
|
||||
if (jsonObject == null) {
|
||||
AppText("不支持编辑")
|
||||
return
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.58f),
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(
|
||||
items = jsonObject.entrySet().toList(),
|
||||
key = { it.key }
|
||||
) { entry ->
|
||||
BatchImportJsonField(
|
||||
name = entry.key,
|
||||
value = entry.value,
|
||||
onValueChange = { value ->
|
||||
val updatedJsonObject = data.toImportJsonObject() ?: return@BatchImportJsonField
|
||||
updatedJsonObject.add(entry.key, value)
|
||||
updatedJsonObject.toImportDataLike(data)?.let(onDataChange)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BatchImportJsonField(
|
||||
name: String,
|
||||
value: JsonElement,
|
||||
onValueChange: (JsonElement) -> Unit
|
||||
) {
|
||||
val primitive = value.takeIf { it.isJsonPrimitive }?.asJsonPrimitive
|
||||
if (primitive?.isBoolean == true) {
|
||||
GlassCard(
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent
|
||||
) {
|
||||
SwitchSettingItem(
|
||||
title = name,
|
||||
checked = primitive.asBoolean,
|
||||
onCheckedChange = { onValueChange(JsonPrimitive(it)) }
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val isJsonText = value.isJsonObject || value.isJsonArray
|
||||
val initialText = value.toImportEditText()
|
||||
var text by remember(name, initialText) { mutableStateOf(initialText) }
|
||||
|
||||
AppTextField(
|
||||
value = text,
|
||||
onValueChange = { newText ->
|
||||
text = newText
|
||||
newText.toImportJsonElement(value)?.let(onValueChange)
|
||||
},
|
||||
label = name,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = !isJsonText,
|
||||
maxLines = if (isJsonText) 8 else 1
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImportItemRow(
|
||||
title: String,
|
||||
@@ -220,7 +323,7 @@ fun ImportItemRow(
|
||||
isSelected = isSelected,
|
||||
inSelectionMode = true,
|
||||
onToggleSelection = onClick,
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
trailingAction = {
|
||||
AppText(
|
||||
text = when (status) {
|
||||
@@ -248,43 +351,49 @@ fun ImportItemRow(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun ImportBottomBar(
|
||||
selectedCount: Int,
|
||||
totalCount: Int,
|
||||
onToggleSelectAll: (Boolean) -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val allSelected = selectedCount == totalCount
|
||||
OutlinedToggleButton(
|
||||
checked = allSelected,
|
||||
onCheckedChange = { checked ->
|
||||
onToggleSelectAll(checked)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = if (allSelected) "全不选" else "全选"
|
||||
)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = onCancel) { AppText("取消") }
|
||||
Button(
|
||||
enabled = selectedCount > 0,
|
||||
onClick = onConfirm
|
||||
) {
|
||||
AppText("导入")
|
||||
private fun Any?.toImportJsonObject(): JsonObject? {
|
||||
return GSON.toJsonTree(this).takeIf { it.isJsonObject }?.asJsonObject
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> JsonObject.toImportDataLike(data: T): T? {
|
||||
val clazz = data?.let { it::class.java } ?: return null
|
||||
return runCatching { GSON.fromJson(this, clazz) as T }.getOrNull()
|
||||
}
|
||||
|
||||
private fun JsonElement.toImportEditText(): String {
|
||||
return when {
|
||||
this is JsonNull || isJsonNull -> ""
|
||||
isJsonObject || isJsonArray -> GSON.toJson(this)
|
||||
isJsonPrimitive -> asJsonPrimitive.asString
|
||||
else -> toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toImportJsonElement(oldValue: JsonElement): JsonElement? {
|
||||
val text = trim()
|
||||
if (oldValue.isJsonNull) {
|
||||
return if (text.isEmpty()) JsonNull.INSTANCE else JsonPrimitive(this)
|
||||
}
|
||||
|
||||
if (oldValue.isJsonObject || oldValue.isJsonArray) {
|
||||
if (text.isEmpty()) return JsonNull.INSTANCE
|
||||
return runCatching { JsonParser.parseString(this) }.getOrNull()
|
||||
}
|
||||
|
||||
if (!oldValue.isJsonPrimitive) return JsonPrimitive(this)
|
||||
|
||||
val primitive = oldValue.asJsonPrimitive
|
||||
return when {
|
||||
primitive.isNumber -> {
|
||||
if (text.isEmpty()) {
|
||||
JsonNull.INSTANCE
|
||||
} else {
|
||||
text.toLongOrNull()?.let { JsonPrimitive(it) }
|
||||
?: text.toDoubleOrNull()?.let { JsonPrimitive(it) }
|
||||
}
|
||||
}
|
||||
|
||||
else -> JsonPrimitive(this)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -24,9 +24,10 @@ sealed interface BaseImportUiState<out T> {
|
||||
data class Success<T>(
|
||||
val source: String,
|
||||
val items: List<ImportItemWrapper<T>>,
|
||||
val version: Int = 0,
|
||||
// 导入配置项
|
||||
val keepOriginalName: Boolean = false,
|
||||
val customGroup: String? = null,
|
||||
val isAddGroup: Boolean = false
|
||||
) : BaseImportUiState<T>
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user