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 package li.songe.gkd.ui.component
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width 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.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
@ -13,27 +17,52 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment 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.graphics.Color
import androidx.compose.ui.platform.LocalContext
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.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.RawSubscription
import li.songe.gkd.data.SubsItem 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.formatTimeAgo
import li.songe.gkd.util.json
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.map 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.subsLoadErrorsFlow
import li.songe.gkd.util.subsRefreshErrorsFlow import li.songe.gkd.util.subsRefreshErrorsFlow
import li.songe.gkd.util.subsRefreshingFlow import li.songe.gkd.util.subsRefreshingFlow
import li.songe.gkd.util.toast
@Composable @Composable
fun SubsItemCard( fun SubsItemCard(
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource,
subsItem: SubsItem, subsItem: SubsItem,
rawSubscription: RawSubscription?, subscription: RawSubscription?,
index: Int, index: Int,
vm: ViewModel,
onCheckedChange: ((Boolean) -> Unit)? = null, onCheckedChange: ((Boolean) -> Unit)? = null,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -44,82 +73,220 @@ fun SubsItemCard(
subsRefreshErrorsFlow.map(scope) { it[subsItem.id] } subsRefreshErrorsFlow.map(scope) { it[subsItem.id] }
}.collectAsState() }.collectAsState()
val subsRefreshing by subsRefreshingFlow.collectAsState() val subsRefreshing by subsRefreshingFlow.collectAsState()
var expanded by remember { mutableStateOf(false) }
Row( Card(
verticalAlignment = Alignment.CenterVertically, onClick = {
modifier = Modifier.padding(8.dp) if (!subsRefreshingFlow.value) {
expanded = true
}
},
modifier = modifier.padding(16.dp, 2.dp),
shape = MaterialTheme.shapes.small,
interactionSource = interactionSource,
) { ) {
Column( SubsMenuItem(
modifier = Modifier.weight(1f), expanded = expanded,
verticalArrangement = Arrangement.spacedBy(4.dp), onExpandedChange = { expanded = it },
subItem = subsItem,
subscription = subscription,
vm = vm
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(8.dp),
) { ) {
if (rawSubscription != null) { Column(
Text( modifier = Modifier.weight(1f),
text = index.toString() + ". " + (rawSubscription.name), verticalArrangement = Arrangement.spacedBy(4.dp),
maxLines = 1, ) {
softWrap = false, if (subscription != null) {
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyLarge,
)
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Text( Text(
text = subsItem.sourceText, text = index.toString() + ". " + (subscription.name),
style = MaterialTheme.typography.bodyMedium maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyLarge,
) )
Text( Row(
text = formatTimeAgo(subsItem.mtime), horizontalArrangement = Arrangement.spacedBy(12.dp),
style = MaterialTheme.typography.bodyMedium, ) {
)
if (subsItem.id >= 0) {
Text( Text(
text = "v" + (rawSubscription.version.toString()), text = subsItem.sourceText,
style = MaterialTheme.typography.bodyMedium
)
Text(
text = formatTimeAgo(subsItem.mtime),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
if (subsItem.id >= 0) {
Text(
text = "v" + (subscription.version.toString()),
style = MaterialTheme.typography.bodyMedium,
)
}
} }
} Text(
Text( text = subscription.numText,
text = rawSubscription.numText, style = MaterialTheme.typography.bodyMedium,
style = MaterialTheme.typography.bodyMedium, color = if (subscription.groupsSize == 0) {
color = if (rawSubscription.groupsSize == 0) { LocalContentColor.current.copy(alpha = 0.5f)
LocalContentColor.current.copy(alpha = 0.5f) } else {
} else { LocalContentColor.current
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
} else { } 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( if (subsRefreshError != null) {
text = subsLoadError?.message Text(
?: if (subsRefreshing) "加载中..." else "文件不存在", text = "加载错误: ${subsRefreshError?.message}",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = color 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) { if (subItem.id < 0 || subscription.categories.isNotEmpty()) {
Text( DropdownMenuItem(
text = "加载错误: ${subsRefreshError?.message}", text = {
style = MaterialTheme.typography.bodyMedium, Text(text = "规则类别")
color = MaterialTheme.colorScheme.error },
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)) DropdownMenuItem(
Switch( text = {
checked = subsItem.enable, Text(text = "导出数据")
onCheckedChange = onCheckedChange, },
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.content.Intent
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -49,43 +44,27 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.ClipboardUtils
import com.blankj.utilcode.util.UriUtils import com.blankj.utilcode.util.UriUtils
import com.blankj.utilcode.util.ZipUtils
import com.dylanc.activityresult.launcher.launchForResult import com.dylanc.activityresult.launcher.launchForResult
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch 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.TransferData
import li.songe.gkd.data.exportTransferData
import li.songe.gkd.data.importTransferData import li.songe.gkd.data.importTransferData
import li.songe.gkd.db.DbSet import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.component.SubsItemCard import li.songe.gkd.ui.component.SubsItemCard
import li.songe.gkd.ui.component.getDialogResult 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.LocalLauncher
import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.checkSubsUpdate import li.songe.gkd.util.checkSubsUpdate
import li.songe.gkd.util.exportZipDir
import li.songe.gkd.util.isSafeUrl import li.songe.gkd.util.isSafeUrl
import li.songe.gkd.util.json import li.songe.gkd.util.json
import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry 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.readFileZipByteArray
import li.songe.gkd.util.shareFile
import li.songe.gkd.util.subsFolder
import li.songe.gkd.util.subsIdToRawFlow import li.songe.gkd.util.subsIdToRawFlow
import li.songe.gkd.util.subsItemsFlow import li.songe.gkd.util.subsItemsFlow
import li.songe.gkd.util.subsRefreshingFlow import li.songe.gkd.util.subsRefreshingFlow
@ -99,8 +78,6 @@ val subsNav = BottomNavItem(
@Composable @Composable
fun useSubsManagePage(): ScaffoldExt { fun useSubsManagePage(): ScaffoldExt {
val context = LocalContext.current
val navController = LocalNavController.current
val launcher = LocalLauncher.current val launcher = LocalLauncher.current
val vm = hiltViewModel<HomeVm>() val vm = hiltViewModel<HomeVm>()
@ -114,127 +91,12 @@ fun useSubsManagePage(): ScaffoldExt {
orderSubItems = subItems orderSubItems = subItems
} }
var menuSubItem: SubsItem? by remember { mutableStateOf(null) }
var showAddLinkDialog by remember { mutableStateOf(false) } var showAddLinkDialog by remember { mutableStateOf(false) }
var link by remember { mutableStateOf("") } var link by remember { mutableStateOf("") }
val refreshing by subsRefreshingFlow.collectAsState() val refreshing by subsRefreshingFlow.collectAsState()
val pullRefreshState = rememberPullRefreshState(refreshing, { checkSubsUpdate(true) }) 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) { LaunchedEffect(showAddLinkDialog) {
if (!showAddLinkDialog) { if (!showAddLinkDialog) {
link = "" link = ""
@ -405,12 +267,7 @@ fun useSubsManagePage(): ScaffoldExt {
enabled = !refreshing, enabled = !refreshing,
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Card( SubsItemCard(
onClick = {
if (!refreshing) {
menuSubItem = subItem
}
},
modifier = Modifier modifier = Modifier
.longPressDraggableHandle( .longPressDraggableHandle(
enabled = !refreshing, enabled = !refreshing,
@ -425,22 +282,18 @@ fun useSubsManagePage(): ScaffoldExt {
} }
} }
}, },
) ),
.padding(vertical = 3.dp, horizontal = 8.dp),
shape = RoundedCornerShape(8.dp),
interactionSource = interactionSource, interactionSource = interactionSource,
) { subsItem = subItem,
SubsItemCard( subscription = subsIdToRaw[subItem.id],
subsItem = subItem, index = index + 1,
rawSubscription = subsIdToRaw[subItem.id], vm = vm,
index = index + 1, onCheckedChange = { checked ->
onCheckedChange = { checked -> vm.viewModelScope.launch {
vm.viewModelScope.launch { DbSet.subsItemDao.updateEnable(subItem.id, checked)
DbSet.subsItemDao.updateEnable(subItem.id, checked) }
} },
}, )
)
}
} }
} }
item { item {