diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ce15fc7d8..42e90ff17 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -153,6 +153,7 @@ android { "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", + "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", ) } dependenciesInfo.includeInApk = false diff --git a/app/src/main/kotlin/li/songe/gkd/MainActivity.kt b/app/src/main/kotlin/li/songe/gkd/MainActivity.kt index cdace5a7f..7605257cb 100644 --- a/app/src/main/kotlin/li/songe/gkd/MainActivity.kt +++ b/app/src/main/kotlin/li/songe/gkd/MainActivity.kt @@ -49,6 +49,8 @@ import li.songe.gkd.service.ManageService import li.songe.gkd.service.fixRestartService import li.songe.gkd.service.updateLauncherAppId import li.songe.gkd.ui.component.BuildDialog +import li.songe.gkd.ui.component.ShareDataDialog +import li.songe.gkd.ui.component.SubsSheet import li.songe.gkd.ui.theme.AppTheme import li.songe.gkd.util.EditGithubCookieDlg import li.songe.gkd.util.LocalNavController @@ -108,6 +110,9 @@ class MainActivity : ComponentActivity() { if (META.updateEnabled) { UpgradeDialog(mainVm.updateStatus) } + SubsSheet(mainVm, mainVm.sheetSubsIdFlow) + ShareDataDialog(mainVm, mainVm.showShareDataIdsFlow) + mainVm.inputSubsLinkOption.ContentDialog() } } } diff --git a/app/src/main/kotlin/li/songe/gkd/MainViewModel.kt b/app/src/main/kotlin/li/songe/gkd/MainViewModel.kt index b6f3d814f..ebb7eefa8 100644 --- a/app/src/main/kotlin/li/songe/gkd/MainViewModel.kt +++ b/app/src/main/kotlin/li/songe/gkd/MainViewModel.kt @@ -3,6 +3,8 @@ package li.songe.gkd import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.blankj.utilcode.util.LogUtils +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -15,14 +17,19 @@ import li.songe.gkd.data.SubsItem import li.songe.gkd.db.DbSet import li.songe.gkd.permission.AuthReason import li.songe.gkd.ui.component.AlertDialogOptions +import li.songe.gkd.ui.component.InputSubsLinkOption import li.songe.gkd.ui.component.UploadOptions import li.songe.gkd.util.LOCAL_SUBS_ID import li.songe.gkd.util.UpdateStatus import li.songe.gkd.util.checkUpdate import li.songe.gkd.util.clearCache +import li.songe.gkd.util.client import li.songe.gkd.util.launchTry import li.songe.gkd.util.map import li.songe.gkd.util.storeFlow +import li.songe.gkd.util.subsItemsFlow +import li.songe.gkd.util.toast +import li.songe.gkd.util.updateSubsMutex import li.songe.gkd.util.updateSubscription class MainViewModel : ViewModel() { @@ -48,6 +55,66 @@ class MainViewModel : ViewModel() { val showEditCookieDlgFlow = MutableStateFlow(false) + val inputSubsLinkOption = InputSubsLinkOption() + + val sheetSubsIdFlow = MutableStateFlow(null) + + val showShareDataIdsFlow = MutableStateFlow?>(null) + + fun addOrModifySubs( + url: String, + oldItem: SubsItem? = null, + ) = viewModelScope.launchTry(Dispatchers.IO) { + if (updateSubsMutex.mutex.isLocked) return@launchTry + updateSubsMutex.withLock { + val subItems = subsItemsFlow.value + val text = try { + client.get(url).bodyAsText() + } catch (e: Exception) { + e.printStackTrace() + LogUtils.d(e) + toast("下载订阅文件失败") + return@launchTry + } + val newSubsRaw = try { + RawSubscription.parse(text) + } catch (e: Exception) { + e.printStackTrace() + LogUtils.d(e) + toast("解析订阅文件失败") + return@launchTry + } + if (oldItem == null) { + if (subItems.any { it.id == newSubsRaw.id }) { + toast("订阅已存在") + return@launchTry + } + } else { + if (oldItem.id != newSubsRaw.id) { + toast("订阅id不对应") + return@launchTry + } + } + if (newSubsRaw.id < 0) { + toast("订阅id不可为${newSubsRaw.id}\n负数id为内部使用") + return@launchTry + } + val newItem = oldItem?.copy(updateUrl = url) ?: SubsItem( + id = newSubsRaw.id, + updateUrl = url, + order = if (subItems.isEmpty()) 1 else (subItems.maxBy { it.order }.order + 1) + ) + updateSubscription(newSubsRaw) + if (oldItem == null) { + DbSet.subsItemDao.insert(newItem) + toast("成功添加订阅") + } else { + DbSet.subsItemDao.update(newItem) + toast("成功修改订阅") + } + } + } + init { viewModelScope.launchTry(Dispatchers.IO) { val subsItems = DbSet.subsItemDao.queryAll() diff --git a/app/src/main/kotlin/li/songe/gkd/data/RawSubscription.kt b/app/src/main/kotlin/li/songe/gkd/data/RawSubscription.kt index 3da11ab6f..715b92e28 100644 --- a/app/src/main/kotlin/li/songe/gkd/data/RawSubscription.kt +++ b/app/src/main/kotlin/li/songe/gkd/data/RawSubscription.kt @@ -85,7 +85,7 @@ data class RawSubscription( map } - private val appGroups by lazy { + val appGroups by lazy { apps.flatMap { a -> a.groups } } diff --git a/app/src/main/kotlin/li/songe/gkd/data/SubsItem.kt b/app/src/main/kotlin/li/songe/gkd/data/SubsItem.kt index 5706471d9..a88c1c2c2 100644 --- a/app/src/main/kotlin/li/songe/gkd/data/SubsItem.kt +++ b/app/src/main/kotlin/li/songe/gkd/data/SubsItem.kt @@ -16,6 +16,7 @@ import kotlinx.serialization.Serializable import li.songe.gkd.appScope import li.songe.gkd.db.DbSet import li.songe.gkd.util.LOCAL_SUBS_IDS +import li.songe.gkd.util.format import li.songe.gkd.util.launchTry import li.songe.gkd.util.subsFolder import li.songe.gkd.util.subsIdToRawFlow @@ -39,6 +40,8 @@ data class SubsItem( val isLocal: Boolean get() = LOCAL_SUBS_IDS.contains(id) + val mtimeStr by lazy { mtime.format("yyyy-MM-dd HH:mm:ss") } + @Dao interface SubsItemDao { @Update diff --git a/app/src/main/kotlin/li/songe/gkd/ui/ActionLogPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/ActionLogPage.kt index c77fcae57..1b4f59a12 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/ActionLogPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/ActionLogPage.kt @@ -78,6 +78,7 @@ import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.subsIdToRawFlow +import li.songe.gkd.util.subsItemsFlow import li.songe.gkd.util.throttle import li.songe.gkd.util.toast @@ -143,6 +144,7 @@ fun ActionLogPage() { Icon( imageVector = Icons.Outlined.Delete, contentDescription = null, + tint = MaterialTheme.colorScheme.error ) } } @@ -322,6 +324,7 @@ private fun ActionLogCard( lastItem: Tuple3?, onClick: () -> Unit ) { + val context = LocalContext.current as MainActivity val (actionLog, group, rule) = item val lastActionLog = lastItem?.t0 val isDiffApp = actionLog.appId != lastActionLog?.appId @@ -375,7 +378,16 @@ private fun ActionLogCard( color = LocalContentColor.current.copy(alpha = 0.5f), ) } - Text(text = subscription?.name ?: actionLog.subsId.toString()) + Text( + text = subscription?.name ?: "id=${actionLog.subsId}", + modifier = Modifier.clickable(onClick = throttle { + if (subsItemsFlow.value.any { it.id == actionLog.subsId }) { + context.mainVm.sheetSubsIdFlow.value = actionLog.subsId + } else { + toast("订阅不存在") + } + }) + ) Row( modifier = Modifier.fillMaxWidth() ) { diff --git a/app/src/main/kotlin/li/songe/gkd/ui/ActivityLogPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/ActivityLogPage.kt index 324922ac0..54b422eee 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/ActivityLogPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/ActivityLogPage.kt @@ -96,6 +96,7 @@ fun ActivityLogPage() { Icon( imageVector = Icons.Outlined.Delete, contentDescription = null, + tint = MaterialTheme.colorScheme.error ) } } diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AdvancedPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/AdvancedPage.kt index 482e525e4..93f94ff02 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/AdvancedPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/AdvancedPage.kt @@ -18,7 +18,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.outlined.Edit import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -272,7 +272,7 @@ fun AdvancedPage() { SettingItem( title = "服务端口", subtitle = store.httpServerPort.toString(), - imageVector = Icons.Default.Edit, + imageVector = Icons.Outlined.Edit, onClick = { showEditPortDlg = true } @@ -397,7 +397,7 @@ fun AdvancedPage() { onSuffixClick = { openUri("https://gkd.li?r=1") }, - imageVector = Icons.Default.Edit, + imageVector = Icons.Outlined.Edit, onClick = { context.mainVm.showEditCookieDlgFlow.value = true } diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AppConfigPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/AppConfigPage.kt index fe5d1f21d..063e3b10b 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/AppConfigPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/AppConfigPage.kt @@ -19,7 +19,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Sort -import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.outlined.Edit import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.FloatingActionButton @@ -171,7 +171,7 @@ fun AppConfigPage(appId: String) { }, content = { Icon( - imageVector = Icons.Default.Edit, + imageVector = Icons.Outlined.Edit, contentDescription = null, ) } diff --git a/app/src/main/kotlin/li/songe/gkd/ui/SnapshotPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/SnapshotPage.kt index 1a684a51f..dba3aa827 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/SnapshotPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/SnapshotPage.kt @@ -114,6 +114,7 @@ fun SnapshotPage() { Icon( imageVector = Icons.Outlined.Delete, contentDescription = null, + tint = MaterialTheme.colorScheme.error ) } } diff --git a/app/src/main/kotlin/li/songe/gkd/ui/component/ShareDataDialog.kt b/app/src/main/kotlin/li/songe/gkd/ui/component/ShareDataDialog.kt new file mode 100644 index 000000000..2da00bfc7 --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/ui/component/ShareDataDialog.kt @@ -0,0 +1,70 @@ +package li.songe.gkd.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import li.songe.gkd.MainActivity +import li.songe.gkd.data.exportData +import li.songe.gkd.util.launchTry +import li.songe.gkd.util.saveFileToDownloads +import li.songe.gkd.util.shareFile +import li.songe.gkd.util.throttle + +@Composable +fun ShareDataDialog( + vm: ViewModel, + showShareDataIdsFlow: MutableStateFlow?>, +) { + val showShareDataIds = showShareDataIdsFlow.collectAsState().value + if (showShareDataIds != null) { + val context = LocalContext.current as MainActivity + Dialog(onDismissRequest = { showShareDataIdsFlow.value = null }) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + val modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + Text( + text = "分享到其他应用", modifier = Modifier + .clickable(onClick = throttle { + showShareDataIdsFlow.value = null + vm.viewModelScope.launchTry(Dispatchers.IO) { + val file = exportData(showShareDataIds) + context.shareFile(file, "分享数据文件") + } + }) + .then(modifier) + ) + Text( + text = "保存到下载", + modifier = Modifier + .clickable(onClick = throttle { + showShareDataIdsFlow.value = null + vm.viewModelScope.launchTry(Dispatchers.IO) { + val file = exportData(showShareDataIds) + context.saveFileToDownloads(file) + } + }) + .then(modifier) + ) + } + } + } +} \ No newline at end of file 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 c71598351..47672d21b 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,6 +1,5 @@ package li.songe.gkd.ui.component -import android.view.MotionEvent import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.layout.Arrangement @@ -11,8 +10,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -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 @@ -20,49 +17,27 @@ 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.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInteropFilter -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.lifecycle.viewModelScope -import com.blankj.utilcode.util.ClipboardUtils -import com.ramcosta.composedestinations.generated.destinations.CategoryPageDestination -import com.ramcosta.composedestinations.generated.destinations.GlobalRulePageDestination -import com.ramcosta.composedestinations.generated.destinations.SubsPageDestination -import com.ramcosta.composedestinations.utils.toDestinationsNavigator -import kotlinx.coroutines.Dispatchers +import li.songe.gkd.META import li.songe.gkd.MainActivity import li.songe.gkd.data.RawSubscription import li.songe.gkd.data.SubsItem -import li.songe.gkd.data.deleteSubscription import li.songe.gkd.ui.home.HomeVm -import li.songe.gkd.util.LOCAL_SUBS_ID -import li.songe.gkd.util.LocalNavController -import li.songe.gkd.util.SafeR import li.songe.gkd.util.formatTimeAgo -import li.songe.gkd.util.launchTry import li.songe.gkd.util.map -import li.songe.gkd.util.openUri import li.songe.gkd.util.subsLoadErrorsFlow import li.songe.gkd.util.subsRefreshErrorsFlow import li.songe.gkd.util.throttle -import li.songe.gkd.util.toast import li.songe.gkd.util.updateSubsMutex -@OptIn(ExperimentalComposeUiApi::class) @Composable fun SubsItemCard( modifier: Modifier = Modifier, @@ -76,7 +51,7 @@ fun SubsItemCard( onCheckedChange: ((Boolean) -> Unit), onSelectedChange: (() -> Unit)? = null, ) { - val density = LocalDensity.current + val context = LocalContext.current as MainActivity val subsLoadError by remember(subsItem.id) { subsLoadErrorsFlow.map(vm.viewModelScope) { it[subsItem.id] } }.collectAsState() @@ -84,30 +59,20 @@ fun SubsItemCard( subsRefreshErrorsFlow.map(vm.viewModelScope) { it[subsItem.id] } }.collectAsState() val subsRefreshing by updateSubsMutex.state.collectAsState() - var expanded by remember { mutableStateOf(false) } val dragged by interactionSource.collectIsDraggedAsState() - var clickPositionX by remember { - mutableStateOf(0.dp) - } val onClick = { if (!dragged) { if (isSelectedMode) { onSelectedChange?.invoke() } else if (!updateSubsMutex.mutex.isLocked) { - expanded = true + context.mainVm.sheetSubsIdFlow.value = subsItem.id } } } Card( onClick = onClick, modifier = modifier - .padding(16.dp, 2.dp) - .pointerInteropFilter { event -> - if (event.action == MotionEvent.ACTION_DOWN) { - clickPositionX = with(density) { event.x.toDp() } - } - false - }, + .padding(16.dp, 2.dp), shape = MaterialTheme.shapes.small, interactionSource = interactionSource, colors = CardDefaults.cardColors( @@ -118,15 +83,6 @@ fun SubsItemCard( } ), ) { - SubsMenuItem( - expanded = expanded, - onExpandedChange = { expanded = it }, - subItem = subsItem, - subscription = subscription, - offsetX = clickPositionX, - vm = vm - ) - Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp), @@ -137,7 +93,7 @@ fun SubsItemCard( ) { if (subscription != null) { Text( - text = "$index.${subscription.name}", + text = "$index. ${subscription.name}", maxLines = 1, softWrap = false, overflow = TextOverflow.Ellipsis, @@ -168,7 +124,7 @@ fun SubsItemCard( ) } else { Text( - text = stringResource(SafeR.app_name), + text = META.appName, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.secondary, ) @@ -216,132 +172,25 @@ fun SubsItemCard( } } -@Composable -private fun SubsMenuItem( - expanded: Boolean, - onExpandedChange: ((Boolean) -> Unit), - subItem: SubsItem, - subscription: RawSubscription?, - offsetX: Dp, - vm: HomeVm -) { - val navController = LocalNavController.current - val context = LocalContext.current as MainActivity - val density = LocalDensity.current - var halfMenuWidth by remember { - mutableStateOf(0.dp) - } - DropdownMenu( - expanded = expanded, - onDismissRequest = { onExpandedChange(false) }, - modifier = Modifier.onGloballyPositioned { - halfMenuWidth = with(density) { it.size.width.toDp() } / 2 - }, - offset = DpOffset(if (offsetX < halfMenuWidth) 0.dp else offsetX - halfMenuWidth, 0.dp) - ) { - if (subscription != null) { - if (subItem.id < 0 || subscription.apps.isNotEmpty()) { - DropdownMenuItem( - text = { - Text(text = "应用规则") - }, - onClick = throttle { - onExpandedChange(false) - navController.toDestinationsNavigator() - .navigate(SubsPageDestination(subItem.id)) - } - ) - } - if (subItem.id < 0 || subscription.categories.isNotEmpty()) { - DropdownMenuItem( - text = { - Text(text = "规则类别") - }, - onClick = throttle { - onExpandedChange(false) - navController.toDestinationsNavigator() - .navigate(CategoryPageDestination(subItem.id)) - } - ) - } - if (subItem.id < 0 || subscription.globalGroups.isNotEmpty()) { - DropdownMenuItem( - text = { - Text(text = "全局规则") - }, - onClick = throttle { - onExpandedChange(false) - navController.toDestinationsNavigator() - .navigate(GlobalRulePageDestination(subItem.id)) - } - ) - } - } - subscription?.supportUri?.let { supportUri -> - DropdownMenuItem( - text = { - Text(text = "问题反馈") - }, - onClick = { - onExpandedChange(false) - openUri(supportUri) - } - ) - } - DropdownMenuItem( - text = { - Text(text = "导出数据") - }, - onClick = { - onExpandedChange(false) - vm.viewModelScope.launchTry(Dispatchers.IO) { - vm.showShareDataIdsFlow.value = setOf(subItem.id) - } - } - ) - subItem.updateUrl?.let { - DropdownMenuItem( - text = { - Text(text = "复制链接") - }, - onClick = { - onExpandedChange(false) - ClipboardUtils.copyText(subItem.updateUrl) - toast("复制成功") - } - ) - DropdownMenuItem( - text = { - Text(text = "修改链接") - }, - onClick = { - onExpandedChange(false) - vm.viewModelScope.launchTry { - val newUrl = vm.inputSubsLinkOption.getResult(initValue = it) - newUrl ?: return@launchTry - vm.addOrModifySubs(newUrl, subItem) - } - } - ) - } - if (subItem.id != LOCAL_SUBS_ID) { - DropdownMenuItem( - text = { - Text(text = "删除订阅", color = MaterialTheme.colorScheme.error) - }, - onClick = { - onExpandedChange(false) - vm.viewModelScope.launchTry { - context.mainVm.dialogFlow.waitResult( - title = "删除订阅", - text = "确定删除 ${subscription?.name ?: subItem.id} ?", - error = true, - ) - deleteSubscription(subItem.id) - } - } - ) - } - } -} + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/kotlin/li/songe/gkd/ui/component/SubsSheet.kt b/app/src/main/kotlin/li/songe/gkd/ui/component/SubsSheet.kt new file mode 100644 index 000000000..2edf958f4 --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/ui/component/SubsSheet.kt @@ -0,0 +1,463 @@ +package li.songe.gkd.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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.ramcosta.composedestinations.generated.destinations.CategoryPageDestination +import com.ramcosta.composedestinations.generated.destinations.CategoryPageDestination.invoke +import com.ramcosta.composedestinations.generated.destinations.GlobalRulePageDestination +import com.ramcosta.composedestinations.generated.destinations.GlobalRulePageDestination.invoke +import com.ramcosta.composedestinations.generated.destinations.SubsPageDestination +import com.ramcosta.composedestinations.generated.destinations.SubsPageDestination.invoke +import com.ramcosta.composedestinations.utils.toDestinationsNavigator +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import li.songe.gkd.META +import li.songe.gkd.MainActivity +import li.songe.gkd.data.deleteSubscription +import li.songe.gkd.ui.style.EmptyHeight +import li.songe.gkd.ui.style.itemHorizontalPadding +import li.songe.gkd.ui.style.itemVerticalPadding +import li.songe.gkd.util.LOCAL_SUBS_ID +import li.songe.gkd.util.LocalNavController +import li.songe.gkd.util.checkSubsUpdate +import li.songe.gkd.util.copyText +import li.songe.gkd.util.launchTry +import li.songe.gkd.util.openUri +import li.songe.gkd.util.subsIdToRawFlow +import li.songe.gkd.util.subsItemsFlow +import li.songe.gkd.util.throttle +import li.songe.gkd.util.toast +import li.songe.gkd.util.updateSubsMutex + +@Composable +fun SubsSheet( + vm: ViewModel, + sheetSubsIdFlow: MutableStateFlow +) { + val subsItems by subsItemsFlow.collectAsState() + val (subsId, setSubsId) = remember { mutableStateOf(sheetSubsIdFlow.value) } + val subsItem = subsItems.find { it.id == subsId } + if (subsItem == null) { + LaunchedEffect(null) { + sheetSubsIdFlow.collect { + setSubsId(it) + } + } + } else { + val context = LocalContext.current as MainActivity + val navController = LocalNavController.current + val subsIdToRaw by subsIdToRawFlow.collectAsState() + var swipeEnabled by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + confirmValueChange = { swipeEnabled } + ) + LaunchedEffect(null) { + sheetSubsIdFlow.collect { + if (it == null && sheetState.isVisible) { + launch { + sheetState.hide() + }.invokeOnCompletion { + if (!sheetState.isVisible) { + setSubsId(null) + } + } + } else { + setSubsId(it) + } + } + } + val scrollState = rememberScrollState() + LaunchedEffect(scrollState.value) { + swipeEnabled = scrollState.value == 0 + } + ModalBottomSheet( + onDismissRequest = { + sheetSubsIdFlow.value = null + }, + sheetState = sheetState + ) { + val subscription = subsIdToRaw[subsItem.id] + val showName = subscription?.name ?: "id=${subsItem.id}" + val childModifier = remember { + Modifier + .fillMaxWidth() + .padding(horizontal = itemHorizontalPadding, vertical = itemVerticalPadding / 2) + } + Column( + modifier = Modifier + .verticalScroll( + state = scrollState, + enabled = sheetState.currentValue == SheetValue.Expanded + ) + .fillMaxWidth(), + ) { + Text( + text = showName, + style = MaterialTheme.typography.titleLarge, + modifier = childModifier + ) + if (subscription != null) { + if (!subsItem.isLocal) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = childModifier + ) { + Column { + Text( + text = "作者", + style = MaterialTheme.typography.labelLarge, + ) + Text( + text = subscription.author ?: "未知", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.let { + if (subscription.author == null) { + it.copy(alpha = 0.5f) + } else { + it + } + }, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, + ) + } + Column( + horizontalAlignment = Alignment.End + ) { + Text( + text = "v${subscription.version}", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.tertiary, + modifier = Modifier + .clip(MaterialTheme.shapes.extraSmall) + .background(MaterialTheme.colorScheme.tertiaryContainer) + .padding(horizontal = 2.dp), + ) + } + } + } else { + Column( + modifier = childModifier + ) { + Text( + text = "作者", + style = MaterialTheme.typography.labelLarge, + ) + Text( + text = META.appName, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.secondary, + ) + } + } + Column( + modifier = childModifier + ) { + Text( + text = "更新时间", + style = MaterialTheme.typography.labelLarge, + ) + Text( + text = subsItem.mtimeStr, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (subscription.globalGroups.isNotEmpty() || subsItem.isLocal) { + Row( + modifier = Modifier + .clickable(onClick = throttle { + setSubsId(null) + sheetSubsIdFlow.value = null + navController + .toDestinationsNavigator() + .navigate(GlobalRulePageDestination(subsItem.id)) + }) + .then(childModifier), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = "全局规则", + style = MaterialTheme.typography.labelLarge, + ) + Text( + text = if (subscription.globalGroups.isNotEmpty()) "共 ${subscription.globalGroups.size} 全局规则组" else "暂无", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.let { + if (subscription.globalGroups.isEmpty()) { + it.copy(alpha = 0.5f) + } else { + it + } + }, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null + ) + } + } + if (subscription.appGroups.isNotEmpty() || subsItem.isLocal) { + Row( + modifier = Modifier + .clickable(onClick = throttle { + setSubsId(null) + sheetSubsIdFlow.value = null + navController + .toDestinationsNavigator() + .navigate(SubsPageDestination(subsItem.id)) + }) + .then(childModifier), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = "应用规则", + style = MaterialTheme.typography.labelLarge, + ) + Text( + text = if (subscription.appGroups.isNotEmpty()) "共 ${subscription.apps.size} 应用 ${subscription.appGroups.size} 规则组" else "暂无", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.let { + if (subscription.appGroups.isEmpty()) { + it.copy(alpha = 0.5f) + } else { + it + } + }, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null + ) + } + + } + if (subscription.categories.isNotEmpty() || subsItem.isLocal) { + Row( + modifier = Modifier + .clickable(onClick = throttle { + setSubsId(null) + sheetSubsIdFlow.value = null + navController + .toDestinationsNavigator() + .navigate(CategoryPageDestination(subsItem.id)) + }) + .then(childModifier), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = "规则类别", + style = MaterialTheme.typography.labelLarge, + ) + Text( + text = if (subscription.categories.isNotEmpty()) "共 ${subscription.categories.size} 类别" else "暂无", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.let { + if (subscription.categories.isEmpty()) { + it.copy(alpha = 0.5f) + } else { + it + } + }, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null + ) + } + } + if (!subsItem.isLocal && subsItem.updateUrl != null) { + Row( + modifier = Modifier + .clickable(onClick = throttle { + if (updateSubsMutex.mutex.isLocked) { + toast("正在刷新订阅,请稍后操作") + return@throttle + } + context.mainVm.viewModelScope.launchTry { + val url = + context.mainVm.inputSubsLinkOption.getResult(initValue = subsItem.updateUrl) + ?: return@launchTry + context.mainVm.addOrModifySubs(url, subsItem) + } + }) + .then(childModifier), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f), + ) { + Text( + text = "订阅链接", + style = MaterialTheme.typography.labelLarge, + ) + StartEllipsisText( + text = subsItem.updateUrl, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.secondary, + softWrap = false, + modifier = Modifier + .clickable(onClick = throttle { + copyText(subsItem.updateUrl) + }) + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = Icons.Outlined.Edit, + contentDescription = null, + ) + } + } + if (!subsItem.isLocal && subscription.supportUri != null) { + Row( + modifier = Modifier + .clickable(onClick = throttle { + openUri(subscription.supportUri) + }) + .then(childModifier), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = "问题反馈", + style = MaterialTheme.typography.labelLarge, + ) + StartEllipsisText( + text = subscription.supportUri, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.secondary, + softWrap = false, + modifier = Modifier + .clickable(onClick = throttle { + copyText(subscription.supportUri) + }) + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = Icons.AutoMirrored.Filled.OpenInNew, + contentDescription = null, + ) + } + } + } else { + val loading by updateSubsMutex.state.collectAsState() + Column( + modifier = Modifier + .fillMaxWidth() + .height(150.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(EmptyHeight)) + if (loading) { + CircularProgressIndicator() + } else { + Text( + text = "文件加载错误或不存在", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.error, + ) + TextButton(onClick = throttle { checkSubsUpdate(showToast = true) }) { + Text(text = "重新加载") + } + } + } + } + + Row( + modifier = childModifier, + horizontalArrangement = Arrangement.End + ) { + if (subscription != null || !subsItem.isLocal) { + IconButton(onClick = throttle { + context.mainVm.showShareDataIdsFlow.value = setOf(subsItem.id) + }) { + Icon(imageVector = Icons.Default.Share, contentDescription = null) + } + } + if (subsItem.id != LOCAL_SUBS_ID) { + IconButton(onClick = throttle { + vm.viewModelScope.launchTry { + context.mainVm.dialogFlow.waitResult( + title = "删除订阅", + text = "确定删除 ${subscription?.name ?: subsItem.id} ?", + error = true, + ) + sheetSubsIdFlow.value = null + setSubsId(null) + deleteSubscription(subsItem.id) + } + }) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + } + } + } + Spacer(modifier = Modifier.height(EmptyHeight / 2)) + } + } + } +} diff --git a/app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt b/app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt index 1ca99a91d..e14f8b8fc 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt @@ -2,10 +2,6 @@ package li.songe.gkd.ui.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.blankj.utilcode.util.LogUtils -import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsText -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -13,25 +9,16 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import li.songe.gkd.appScope -import li.songe.gkd.data.RawSubscription -import li.songe.gkd.data.SubsItem import li.songe.gkd.db.DbSet -import li.songe.gkd.ui.component.InputSubsLinkOption import li.songe.gkd.util.SortTypeOption import li.songe.gkd.util.actionCountFlow import li.songe.gkd.util.appInfoCacheFlow -import li.songe.gkd.util.client import li.songe.gkd.util.getSubsStatus -import li.songe.gkd.util.launchTry import li.songe.gkd.util.map import li.songe.gkd.util.orderedAppInfosFlow import li.songe.gkd.util.ruleSummaryFlow import li.songe.gkd.util.storeFlow import li.songe.gkd.util.subsIdToRawFlow -import li.songe.gkd.util.subsItemsFlow -import li.songe.gkd.util.toast -import li.songe.gkd.util.updateSubsMutex -import li.songe.gkd.util.updateSubscription class HomeVm : ViewModel() { @@ -64,60 +51,6 @@ class HomeVm : ViewModel() { }.stateIn(appScope, SharingStarted.Eagerly, "") } - fun addOrModifySubs( - url: String, - oldItem: SubsItem? = null, - ) = viewModelScope.launchTry(Dispatchers.IO) { - if (updateSubsMutex.mutex.isLocked) return@launchTry - updateSubsMutex.withLock { - val subItems = subsItemsFlow.value - val text = try { - client.get(url).bodyAsText() - } catch (e: Exception) { - e.printStackTrace() - LogUtils.d(e) - toast("下载订阅文件失败") - return@launchTry - } - val newSubsRaw = try { - RawSubscription.parse(text) - } catch (e: Exception) { - e.printStackTrace() - LogUtils.d(e) - toast("解析订阅文件失败") - return@launchTry - } - if (oldItem == null) { - if (subItems.any { it.id == newSubsRaw.id }) { - toast("订阅已存在") - return@launchTry - } - } else { - if (oldItem.id != newSubsRaw.id) { - toast("订阅id不对应") - return@launchTry - } - } - if (newSubsRaw.id < 0) { - toast("订阅id不可为${newSubsRaw.id}\n负数id为内部使用") - return@launchTry - } - val newItem = oldItem?.copy(updateUrl = url) ?: SubsItem( - id = newSubsRaw.id, - updateUrl = url, - order = if (subItems.isEmpty()) 1 else (subItems.maxBy { it.order }.order + 1) - ) - updateSubscription(newSubsRaw) - if (oldItem == null) { - DbSet.subsItemDao.insert(newItem) - toast("成功添加订阅") - } else { - DbSet.subsItemDao.update(newItem) - toast("成功修改订阅") - } - } - } - private val appIdToOrderFlow = DbSet.actionLogDao.queryLatestUniqueAppIds().map { appIds -> appIds.mapIndexed { index, appId -> appId to index }.toMap() } @@ -171,7 +104,4 @@ class HomeVm : ViewModel() { } }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) - val showShareDataIdsFlow = MutableStateFlow?>(null) - - val inputSubsLinkOption = InputSubsLinkOption() } \ No newline at end of file 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 9e3531616..61e38c228 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 @@ -10,14 +10,12 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding 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 @@ -26,7 +24,6 @@ import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Card import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -55,7 +52,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.dylanc.activityresult.launcher.launchForResult @@ -65,7 +61,6 @@ import kotlinx.coroutines.launch import li.songe.gkd.MainActivity import li.songe.gkd.data.Value import li.songe.gkd.data.deleteSubscription -import li.songe.gkd.data.exportData import li.songe.gkd.data.importData import li.songe.gkd.db.DbSet import li.songe.gkd.ui.component.SubsItemCard @@ -82,8 +77,6 @@ import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchTry import li.songe.gkd.util.map import li.songe.gkd.util.openUri -import li.songe.gkd.util.saveFileToDownloads -import li.songe.gkd.util.shareFile import li.songe.gkd.util.storeFlow import li.songe.gkd.util.subsIdToRawFlow import li.songe.gkd.util.subsItemsFlow @@ -188,9 +181,6 @@ fun useSubsManagePage(): ScaffoldExt { ) } - ShareDataDialog(vm) - vm.inputSubsLinkOption.ContentDialog() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() return ScaffoldExt( navItem = subsNav, @@ -242,11 +232,12 @@ fun useSubsManagePage(): ScaffoldExt { Icon( imageVector = Icons.Outlined.Delete, contentDescription = null, + tint = MaterialTheme.colorScheme.error ) } } IconButton(onClick = { - vm.showShareDataIdsFlow.value = selectedIds + context.mainVm.showShareDataIdsFlow.value = selectedIds }) { Icon( imageVector = Icons.Default.Share, @@ -367,9 +358,9 @@ fun useSubsManagePage(): ScaffoldExt { toast("正在刷新订阅,请稍后操作") return@FloatingActionButton } - vm.viewModelScope.launchTry { - val url = vm.inputSubsLinkOption.getResult() ?: return@launchTry - vm.addOrModifySubs(url) + context.mainVm.viewModelScope.launchTry { + val url = context.mainVm.inputSubsLinkOption.getResult() ?: return@launchTry + context.mainVm.addOrModifySubs(url) } }) { Icon( @@ -495,46 +486,3 @@ fun useSubsManagePage(): ScaffoldExt { } } } - -@Composable -private fun ShareDataDialog(vm: HomeVm) { - val context = LocalContext.current as MainActivity - val showShareDataIds = vm.showShareDataIdsFlow.collectAsState().value - if (showShareDataIds != null) { - Dialog(onDismissRequest = { vm.showShareDataIdsFlow.value = null }) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - shape = RoundedCornerShape(16.dp), - ) { - val modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - Text( - text = "分享到其他应用", modifier = Modifier - .clickable(onClick = throttle { - vm.showShareDataIdsFlow.value = null - vm.viewModelScope.launchTry(Dispatchers.IO) { - val file = exportData(showShareDataIds) - context.shareFile(file, "分享数据文件") - } - }) - .then(modifier) - ) - Text( - text = "保存到下载", - modifier = Modifier - .clickable(onClick = throttle { - vm.showShareDataIdsFlow.value = null - vm.viewModelScope.launchTry(Dispatchers.IO) { - val file = exportData(showShareDataIds) - context.saveFileToDownloads(file) - } - }) - .then(modifier) - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/util/IntentExt.kt b/app/src/main/kotlin/li/songe/gkd/util/IntentExt.kt index 8744cfea4..7dd1688b8 100644 --- a/app/src/main/kotlin/li/songe/gkd/util/IntentExt.kt +++ b/app/src/main/kotlin/li/songe/gkd/util/IntentExt.kt @@ -19,9 +19,9 @@ import li.songe.gkd.permission.canWriteExternalStorage import li.songe.gkd.permission.requiredPermission import java.io.File -fun Context.shareFile(file: File, title: String) { +fun MainActivity.shareFile(file: File, title: String) { val uri = FileProvider.getUriForFile( - this, "${packageName}.provider", file + app, "${app.packageName}.provider", file ) val intent = Intent().apply { action = Intent.ACTION_SEND diff --git a/app/src/main/kotlin/li/songe/gkd/util/SubsState.kt b/app/src/main/kotlin/li/songe/gkd/util/SubsState.kt index 364096587..7e94fab7b 100644 --- a/app/src/main/kotlin/li/songe/gkd/util/SubsState.kt +++ b/app/src/main/kotlin/li/songe/gkd/util/SubsState.kt @@ -315,19 +315,23 @@ private fun loadSubs(id: Long): RawSubscription { return subscription } -private fun refreshRawSubsList(items: List) { +private fun refreshRawSubsList(items: List): Boolean { + if (items.isEmpty()) return false val subscriptions = subsIdToRawFlow.value.toMutableMap() val errors = subsLoadErrorsFlow.value.toMutableMap() + var changed = false items.forEach { s -> try { subscriptions[s.id] = loadSubs(s.id) errors.remove(s.id) + changed = true } catch (e: Exception) { errors[s.id] = e } } subsIdToRawFlow.value = subscriptions subsLoadErrorsFlow.value = errors + return changed } fun initSubsState() { @@ -399,13 +403,14 @@ fun checkSubsUpdate(showToast: Boolean = false) = appScope.launchTry(Dispatchers return@withLock } LogUtils.d("开始检测更新") - val localSubsEntries = - subsEntriesFlow.value.filter { e -> e.subsItem.id < 0 && e.subscription == null } - val subsEntries = subsEntriesFlow.value.filter { e -> e.subsItem.id >= 0 } - refreshRawSubsList(localSubsEntries.map { e -> e.subsItem }) - + // 文件不存在, 重新加载 + val changed = refreshRawSubsList(subsEntriesFlow.value.filter { it.subscription == null } + .map { it.subsItem }) + if (changed) { + delay(500) + } var successNum = 0 - subsEntries.forEach { subsEntry -> + subsEntriesFlow.value.filter { !it.subsItem.isLocal }.forEach { subsEntry -> try { val newSubsRaw = updateSubs(subsEntry) if (newSubsRaw != null) {