From 1b570cd1af889c1f82c25b9cf256e63c2467270e Mon Sep 17 00:00:00 2001 From: lisonge Date: Wed, 29 May 2024 21:40:51 +0800 Subject: [PATCH] perf: subsItem card --- .../li/songe/gkd/ui/component/SubsItemCard.kt | 289 ++++++++++++++---- .../li/songe/gkd/ui/home/SubsManagePage.kt | 171 +---------- 2 files changed, 240 insertions(+), 220 deletions(-) diff --git a/app/src/main/kotlin/li/songe/gkd/ui/component/SubsItemCard.kt b/app/src/main/kotlin/li/songe/gkd/ui/component/SubsItemCard.kt index 9558d7010..588dd6aff 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/component/SubsItemCard.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/component/SubsItemCard.kt @@ -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( - text = formatTimeAgo(subsItem.mtime), - style = MaterialTheme.typography.bodyMedium, + text = index.toString() + ". " + (subscription.name), + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyLarge, ) - 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) + Text( + text = subscription.numText, + style = MaterialTheme.typography.bodyMedium, + color = if (subscription.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 } else { - LocalContentColor.current + Color.Unspecified } - ) - } 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 { - Color.Unspecified + 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 + ) } - Text( - text = subsLoadError?.message - ?: if (subsRefreshing) "加载中..." else "文件不存在", - style = MaterialTheme.typography.bodyMedium, - color = color + } + 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 (subItem.id < 0 || subscription.categories.isNotEmpty()) { + DropdownMenuItem( + text = { + Text(text = "规则类别") + }, + onClick = { + onExpandedChange(false) + navController.navigate(CategoryPageDestination(subItem.id)) + } ) } - if (subsRefreshError != null) { - Text( - text = "加载错误: ${subsRefreshError?.message}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error + 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() + } + } + ) + } } } diff --git a/app/src/main/kotlin/li/songe/gkd/ui/home/SubsManagePage.kt b/app/src/main/kotlin/li/songe/gkd/ui/home/SubsManagePage.kt index e9c70fae1..f96f87bb1 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/home/SubsManagePage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/home/SubsManagePage.kt @@ -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() @@ -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 {