[优化] 修复订阅源视频播放问题和新增源编辑
This commit is contained in:
13 files changed
+354
-124
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,11 +192,20 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
customViewCallback?.onCustomViewHidden()
|
||||||
|
hideCustomView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -321,7 +355,20 @@ fun RssReadRouteScreen(
|
|||||||
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() }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -335,6 +382,22 @@ fun RssReadRouteScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customView?.let { view ->
|
||||||
|
key(view) {
|
||||||
|
AndroidView(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black)
|
||||||
|
.zIndex(1f),
|
||||||
|
factory = {
|
||||||
|
(view.parent as? ViewGroup)?.removeView(view)
|
||||||
|
view
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FavoriteEditSheet(
|
FavoriteEditSheet(
|
||||||
show = showFavoriteSheet,
|
show = showFavoriteSheet,
|
||||||
titleValue = favoriteTitle,
|
titleValue = favoriteTitle,
|
||||||
@@ -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)
|
||||||
) {
|
) {
|
||||||
|
|||||||
+204
-95
@@ -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,64 +127,78 @@ 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
|
||||||
AppModalBottomSheet(
|
val sheetTitle = when {
|
||||||
show = show,
|
isEditing -> itemTitle(editingItem.data)
|
||||||
onDismissRequest = onDismissRequest
|
selectedCount > 0 -> {
|
||||||
) {
|
|
||||||
AppScaffold(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
|
|
||||||
topBar = {
|
|
||||||
CenterAlignedTopAppBar(
|
|
||||||
title = {
|
|
||||||
AnimatedText(
|
|
||||||
if (selectedCount > 0)
|
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.select_count,
|
R.string.select_count,
|
||||||
selectedCount,
|
selectedCount,
|
||||||
totalCount
|
totalCount
|
||||||
)
|
)
|
||||||
else
|
}
|
||||||
title
|
|
||||||
)
|
else -> title
|
||||||
},
|
}
|
||||||
actions = topBarActions,
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
AppModalBottomSheet(
|
||||||
containerColor = Color.Transparent
|
show = show,
|
||||||
)
|
onDismissRequest = onDismissRequest,
|
||||||
)
|
modifier = Modifier.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
|
||||||
},
|
title = sheetTitle,
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
startAction = if (isEditing) {
|
||||||
bottomBar = {
|
{
|
||||||
ImportBottomBar(
|
SmallIconButton(
|
||||||
selectedCount = selectedCount,
|
onClick = { editingIndex = null },
|
||||||
totalCount = totalCount,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
onToggleSelectAll = onToggleAll,
|
contentDescription = "返回"
|
||||||
onConfirm = {
|
|
||||||
val selectedData =
|
|
||||||
currentState.items.filter { it.isSelected }.map { it.data }
|
|
||||||
onConfirm(selectedData)
|
|
||||||
},
|
|
||||||
onCancel = onDismissRequest
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
} 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(
|
LazyColumn(
|
||||||
modifier = Modifier.padding(padding),
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
currentState.items,
|
currentState.items,
|
||||||
key = { _, item -> item.data.hashCode() }) { index, itemWrapper ->
|
key = { _, item -> item.data.hashCode() }
|
||||||
|
) { index, itemWrapper ->
|
||||||
ImportItemRow(
|
ImportItemRow(
|
||||||
title = itemTitle(itemWrapper.data),
|
title = itemTitle(itemWrapper.data),
|
||||||
subtitle = itemSubtitle(itemWrapper.data),
|
subtitle = itemSubtitle(itemWrapper.data),
|
||||||
@@ -195,14 +206,106 @@ fun <T> BatchImportDialog(
|
|||||||
status = itemWrapper.status,
|
status = itemWrapper.status,
|
||||||
onClick = { onToggleItem(index) },
|
onClick = { onToggleItem(index) },
|
||||||
onInfoClick = {
|
onInfoClick = {
|
||||||
scope.launch { snackbarHostState.showSnackbar("其实还没写桀桀桀") }
|
|
||||||
onItemInfoClick(index)
|
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
|
||||||
@@ -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,
|
|
||||||
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("取消") }
|
@Suppress("UNCHECKED_CAST")
|
||||||
Button(
|
private fun <T> JsonObject.toImportDataLike(data: T): T? {
|
||||||
enabled = selectedCount > 0,
|
val clazz = data?.let { it::class.java } ?: return null
|
||||||
onClick = onConfirm
|
return runCatching { GSON.fromJson(this, clazz) as T }.getOrNull()
|
||||||
) {
|
}
|
||||||
AppText("导入")
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ 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,
|
||||||
|
|||||||
Reference in new issue
Block a user