[优化] 修复订阅源视频播放问题和新增源编辑

This commit is contained in:
HapeLee committed 2026-04-29 13:27:46 +08:00
1 parent 914efbb658
commit a191d80e02
13 files changed
+515 -285

No files matched your search

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