perf: subsItem card

This commit is contained in:
lisonge 2024-05-29 21:40:51 +08:00
parent de431cb3c3
commit 1b570cd1af
2 changed files with 241 additions and 221 deletions

View File

@ -1,11 +1,15 @@
package li.songe.gkd.ui.component
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
@ -13,27 +17,52 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.ClipboardUtils
import com.blankj.utilcode.util.ZipUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.encodeToString
import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.TransferData
import li.songe.gkd.data.exportTransferData
import li.songe.gkd.ui.destinations.CategoryPageDestination
import li.songe.gkd.ui.destinations.GlobalRulePageDestination
import li.songe.gkd.ui.destinations.SubsPageDestination
import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.exportZipDir
import li.songe.gkd.util.formatTimeAgo
import li.songe.gkd.util.json
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.map
import li.songe.gkd.util.navigate
import li.songe.gkd.util.openUri
import li.songe.gkd.util.shareFile
import li.songe.gkd.util.subsLoadErrorsFlow
import li.songe.gkd.util.subsRefreshErrorsFlow
import li.songe.gkd.util.subsRefreshingFlow
import li.songe.gkd.util.toast
@Composable
fun SubsItemCard(
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource,
subsItem: SubsItem,
rawSubscription: RawSubscription?,
subscription: RawSubscription?,
index: Int,
vm: ViewModel,
onCheckedChange: ((Boolean) -> Unit)? = null,
) {
val scope = rememberCoroutineScope()
@ -44,82 +73,220 @@ fun SubsItemCard(
subsRefreshErrorsFlow.map(scope) { it[subsItem.id] }
}.collectAsState()
val subsRefreshing by subsRefreshingFlow.collectAsState()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(8.dp)
var expanded by remember { mutableStateOf(false) }
Card(
onClick = {
if (!subsRefreshingFlow.value) {
expanded = true
}
},
modifier = modifier.padding(16.dp, 2.dp),
shape = MaterialTheme.shapes.small,
interactionSource = interactionSource,
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
SubsMenuItem(
expanded = expanded,
onExpandedChange = { expanded = it },
subItem = subsItem,
subscription = subscription,
vm = vm
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(8.dp),
) {
if (rawSubscription != null) {
Text(
text = index.toString() + ". " + (rawSubscription.name),
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyLarge,
)
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (subscription != null) {
Text(
text = subsItem.sourceText,
style = MaterialTheme.typography.bodyMedium
text = index.toString() + ". " + (subscription.name),
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyLarge,
)
Text(
text = formatTimeAgo(subsItem.mtime),
style = MaterialTheme.typography.bodyMedium,
)
if (subsItem.id >= 0) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = "v" + (rawSubscription.version.toString()),
text = subsItem.sourceText,
style = MaterialTheme.typography.bodyMedium
)
Text(
text = formatTimeAgo(subsItem.mtime),
style = MaterialTheme.typography.bodyMedium,
)
if (subsItem.id >= 0) {
Text(
text = "v" + (subscription.version.toString()),
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
Text(
text = rawSubscription.numText,
style = MaterialTheme.typography.bodyMedium,
color = if (rawSubscription.groupsSize == 0) {
LocalContentColor.current.copy(alpha = 0.5f)
} else {
LocalContentColor.current
}
)
} else {
Text(
text = "${index}. id:${subsItem.id}",
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
val color = if (subsLoadError != null) {
MaterialTheme.colorScheme.error
Text(
text = subscription.numText,
style = MaterialTheme.typography.bodyMedium,
color = if (subscription.groupsSize == 0) {
LocalContentColor.current.copy(alpha = 0.5f)
} else {
LocalContentColor.current
}
)
} else {
Color.Unspecified
Text(
text = "${index}. id:${subsItem.id}",
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
val color = if (subsLoadError != null) {
MaterialTheme.colorScheme.error
} else {
Color.Unspecified
}
Text(
text = subsLoadError?.message
?: if (subsRefreshing) "加载中..." else "文件不存在",
style = MaterialTheme.typography.bodyMedium,
color = color
)
}
Text(
text = subsLoadError?.message
?: if (subsRefreshing) "加载中..." else "文件不存在",
style = MaterialTheme.typography.bodyMedium,
color = color
if (subsRefreshError != null) {
Text(
text = "加载错误: ${subsRefreshError?.message}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
}
Spacer(modifier = Modifier.width(10.dp))
Switch(
checked = subsItem.enable,
onCheckedChange = onCheckedChange,
)
}
}
}
@Composable
private fun SubsMenuItem(
expanded: Boolean,
onExpandedChange: ((Boolean) -> Unit),
subItem: SubsItem,
subscription: RawSubscription?,
vm: ViewModel
) {
val navController = LocalNavController.current
val context = LocalContext.current
DropdownMenu(
expanded = expanded,
onDismissRequest = { onExpandedChange(false) }
) {
if (subscription != null) {
if (subItem.id < 0 || subscription.apps.isNotEmpty()) {
DropdownMenuItem(
text = {
Text(text = "应用规则")
},
onClick = {
onExpandedChange(false)
navController.navigate(SubsPageDestination(subItem.id))
}
)
}
if (subsRefreshError != null) {
Text(
text = "加载错误: ${subsRefreshError?.message}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
if (subItem.id < 0 || subscription.categories.isNotEmpty()) {
DropdownMenuItem(
text = {
Text(text = "规则类别")
},
onClick = {
onExpandedChange(false)
navController.navigate(CategoryPageDestination(subItem.id))
}
)
}
if (subItem.id < 0 || subscription.globalGroups.isNotEmpty()) {
DropdownMenuItem(
text = {
Text(text = "全局规则")
},
onClick = {
onExpandedChange(false)
navController.navigate(GlobalRulePageDestination(subItem.id))
}
)
}
}
Spacer(modifier = Modifier.width(10.dp))
Switch(
checked = subsItem.enable,
onCheckedChange = onCheckedChange,
DropdownMenuItem(
text = {
Text(text = "导出数据")
},
onClick = {
onExpandedChange(false)
vm.viewModelScope.launchTry(Dispatchers.IO) {
val transferDataFile = exportZipDir.resolve("${TransferData.TYPE}.json")
transferDataFile.writeText(
json.encodeToString(
exportTransferData(
listOf(
subItem.id
)
)
)
)
val file = exportZipDir.resolve("backup-${subItem.id}.zip")
if (file.exists()) {
file.delete()
}
ZipUtils.zipFiles(listOf(transferDataFile), file)
transferDataFile.delete()
context.shareFile(file, "分享数据文件")
}
}
)
subItem.updateUrl?.let {
DropdownMenuItem(
text = {
Text(text = "复制链接")
},
onClick = {
onExpandedChange(false)
ClipboardUtils.copyText(subItem.updateUrl)
toast("复制成功")
}
)
}
subscription?.supportUri?.let { supportUri ->
DropdownMenuItem(
text = {
Text(text = "问题反馈")
},
onClick = {
onExpandedChange(false)
context.openUri(supportUri)
}
)
}
if (subItem.id != -2L) {
DropdownMenuItem(
text = {
Text(text = "删除订阅", color = MaterialTheme.colorScheme.error)
},
onClick = {
onExpandedChange(false)
vm.viewModelScope.launchTry {
val result = getDialogResult(
"删除订阅",
"是否删除订阅 ${subscription?.name ?: subItem.id} ?",
)
if (!result) return@launchTry
subItem.removeAssets()
}
}
)
}
}
}

View File

@ -2,7 +2,6 @@ package li.songe.gkd.ui.home
import android.content.Intent
import android.webkit.URLUtil
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -16,21 +15,17 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -49,43 +44,27 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.ClipboardUtils
import com.blankj.utilcode.util.UriUtils
import com.blankj.utilcode.util.ZipUtils
import com.dylanc.activityresult.launcher.launchForResult
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.TransferData
import li.songe.gkd.data.exportTransferData
import li.songe.gkd.data.importTransferData
import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.component.SubsItemCard
import li.songe.gkd.ui.component.getDialogResult
import li.songe.gkd.ui.destinations.CategoryPageDestination
import li.songe.gkd.ui.destinations.GlobalRulePageDestination
import li.songe.gkd.ui.destinations.SubsPageDestination
import li.songe.gkd.util.LocalLauncher
import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.checkSubsUpdate
import li.songe.gkd.util.exportZipDir
import li.songe.gkd.util.isSafeUrl
import li.songe.gkd.util.json
import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.navigate
import li.songe.gkd.util.openUri
import li.songe.gkd.util.readFileZipByteArray
import li.songe.gkd.util.shareFile
import li.songe.gkd.util.subsFolder
import li.songe.gkd.util.subsIdToRawFlow
import li.songe.gkd.util.subsItemsFlow
import li.songe.gkd.util.subsRefreshingFlow
@ -99,8 +78,6 @@ val subsNav = BottomNavItem(
@Composable
fun useSubsManagePage(): ScaffoldExt {
val context = LocalContext.current
val navController = LocalNavController.current
val launcher = LocalLauncher.current
val vm = hiltViewModel<HomeVm>()
@ -114,127 +91,12 @@ fun useSubsManagePage(): ScaffoldExt {
orderSubItems = subItems
}
var menuSubItem: SubsItem? by remember { mutableStateOf(null) }
var showAddLinkDialog by remember { mutableStateOf(false) }
var link by remember { mutableStateOf("") }
val refreshing by subsRefreshingFlow.collectAsState()
val pullRefreshState = rememberPullRefreshState(refreshing, { checkSubsUpdate(true) })
menuSubItem?.let { menuSubItemVal ->
Dialog(onDismissRequest = { menuSubItem = null }) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
val subsRawVal = subsIdToRaw[menuSubItemVal.id]
if (subsRawVal != null) {
Text(text = "应用规则", modifier = Modifier
.clickable {
menuSubItem = null
navController.navigate(SubsPageDestination(subsRawVal.id))
}
.fillMaxWidth()
.padding(16.dp))
HorizontalDivider()
Text(text = "查看类别", modifier = Modifier
.clickable {
menuSubItem = null
navController.navigate(CategoryPageDestination(subsRawVal.id))
}
.fillMaxWidth()
.padding(16.dp))
HorizontalDivider()
Text(text = "全局规则", modifier = Modifier
.clickable {
menuSubItem = null
navController.navigate(GlobalRulePageDestination(subsRawVal.id))
}
.fillMaxWidth()
.padding(16.dp))
HorizontalDivider()
}
Text(text = "导出数据", modifier = Modifier
.clickable {
menuSubItem = null
vm.viewModelScope.launchTry(Dispatchers.IO) {
val transferDataFile = exportZipDir.resolve("${TransferData.TYPE}.json")
transferDataFile.writeText(
json.encodeToString(
exportTransferData(
listOf(
menuSubItemVal.id
)
)
)
)
val file =
exportZipDir.resolve("backup-${System.currentTimeMillis()}.zip")
ZipUtils.zipFiles(listOf(transferDataFile), file)
transferDataFile.delete()
context.shareFile(file, "分享数据文件")
}
}
.fillMaxWidth()
.padding(16.dp))
HorizontalDivider()
if (menuSubItemVal.id < 0 && subsRawVal != null) {
Text(text = "分享文件", modifier = Modifier
.clickable {
menuSubItem = null
vm.viewModelScope.launchTry {
val subsFile = subsFolder.resolve("${menuSubItemVal.id}.json")
context.shareFile(subsFile, "分享订阅文件")
}
}
.fillMaxWidth()
.padding(16.dp))
HorizontalDivider()
}
if (menuSubItemVal.updateUrl != null) {
Text(text = "复制链接", modifier = Modifier
.clickable {
menuSubItem = null
ClipboardUtils.copyText(menuSubItemVal.updateUrl)
toast("复制成功")
}
.fillMaxWidth()
.padding(16.dp))
HorizontalDivider()
}
if (subsRawVal?.supportUri != null) {
Text(text = "问题反馈", modifier = Modifier
.clickable {
menuSubItem = null
context.openUri(subsRawVal.supportUri)
}
.fillMaxWidth()
.padding(16.dp))
HorizontalDivider()
}
if (menuSubItemVal.id != -2L) {
Text(text = "删除订阅", modifier = Modifier
.clickable {
menuSubItem = null
vm.viewModelScope.launchTry {
val result = getDialogResult(
"删除订阅",
"是否删除订阅 ${subsIdToRaw[menuSubItemVal.id]?.name} ?",
)
if (!result) return@launchTry
menuSubItemVal.removeAssets()
}
}
.fillMaxWidth()
.padding(16.dp), color = MaterialTheme.colorScheme.error)
}
}
}
}
LaunchedEffect(showAddLinkDialog) {
if (!showAddLinkDialog) {
link = ""
@ -405,12 +267,7 @@ fun useSubsManagePage(): ScaffoldExt {
enabled = !refreshing,
) {
val interactionSource = remember { MutableInteractionSource() }
Card(
onClick = {
if (!refreshing) {
menuSubItem = subItem
}
},
SubsItemCard(
modifier = Modifier
.longPressDraggableHandle(
enabled = !refreshing,
@ -425,22 +282,18 @@ fun useSubsManagePage(): ScaffoldExt {
}
}
},
)
.padding(vertical = 3.dp, horizontal = 8.dp),
shape = RoundedCornerShape(8.dp),
),
interactionSource = interactionSource,
) {
SubsItemCard(
subsItem = subItem,
rawSubscription = subsIdToRaw[subItem.id],
index = index + 1,
onCheckedChange = { checked ->
vm.viewModelScope.launch {
DbSet.subsItemDao.updateEnable(subItem.id, checked)
}
},
)
}
subsItem = subItem,
subscription = subsIdToRaw[subItem.id],
index = index + 1,
vm = vm,
onCheckedChange = { checked ->
vm.viewModelScope.launch {
DbSet.subsItemDao.updateEnable(subItem.id, checked)
}
},
)
}
}
item {