diff --git a/app/src/main/kotlin/li/songe/gkd/ui/component/InputSubsLinkOption.kt b/app/src/main/kotlin/li/songe/gkd/ui/component/InputSubsLinkOption.kt new file mode 100644 index 0000000..5f31214 --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/ui/component/InputSubsLinkOption.kt @@ -0,0 +1,127 @@ +package li.songe.gkd.ui.component + +import android.webkit.URLUtil +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import li.songe.gkd.MainActivity +import li.songe.gkd.MainViewModel +import li.songe.gkd.util.isSafeUrl +import li.songe.gkd.util.launchAsFn +import li.songe.gkd.util.subsItemsFlow +import li.songe.gkd.util.throttle +import li.songe.gkd.util.toast +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + + +class InputSubsLinkOption { + private val showFlow = MutableStateFlow(false) + private val valueFlow = MutableStateFlow("") + private val initValueFlow = MutableStateFlow("") + private var continuation: Continuation? = null + + private fun resume(value: String?) { + showFlow.value = false + valueFlow.value = "" + initValueFlow.value = "" + continuation?.resume(value) + continuation = null + } + + private suspend fun submit(mainVm: MainViewModel) { + val value = valueFlow.value + if (!URLUtil.isNetworkUrl(value)) { + toast("非法链接") + return + } + val initValue = initValueFlow.value + if (initValue.isNotEmpty() && initValue == value) { + toast("未修改") + resume(null) + return + } + if (subsItemsFlow.value.any { it.updateUrl == value }) { + toast("已有相同链接订阅") + return + } + if (!isSafeUrl(value)) { + mainVm.dialogFlow.waitResult( + title = "未知来源", + text = "你正在添加一个未验证的远程订阅\n\n这可能含有恶意的规则\n\n是否仍然确认添加?" + ) + } + resume(value) + } + + private fun cancel() = resume(null) + + suspend fun getResult(initValue: String = ""): String? { + initValueFlow.value = initValue + valueFlow.value = initValue + showFlow.value = true + return suspendCoroutine { + continuation = it + } + } + + @Composable + fun ContentDialog() { + val show by showFlow.collectAsState() + if (show) { + val context = LocalContext.current as MainActivity + val value by valueFlow.collectAsState() + val initValue by initValueFlow.collectAsState() + AlertDialog( + title = { + Text(text = if (initValue.isNotEmpty()) "修改订阅" else "添加订阅") + }, + text = { + OutlinedTextField( + value = value, + onValueChange = { + valueFlow.value = it.trim() + }, + maxLines = 8, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text(text = "请输入订阅链接") + }, + isError = value.isNotEmpty() && !URLUtil.isNetworkUrl(value), + ) + }, + onDismissRequest = { + if (valueFlow.value.isEmpty()) { + cancel() + } + }, + confirmButton = { + TextButton( + enabled = value.isNotEmpty(), + onClick = throttle(fn = context.mainVm.viewModelScope.launchAsFn { + submit(context.mainVm) + }), + ) { + Text(text = "确定") + } + }, + dismissButton = { + TextButton(onClick = ::cancel) { + Text(text = "取消") + } + }, + ) + } + } +} + 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 3e057ee..d9fb770 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 @@ -237,7 +237,8 @@ private fun SubsMenuItem( }, onClick = throttle { onExpandedChange(false) - navController.toDestinationsNavigator().navigate(SubsPageDestination(subItem.id)) + navController.toDestinationsNavigator() + .navigate(SubsPageDestination(subItem.id)) } ) } @@ -248,7 +249,8 @@ private fun SubsMenuItem( }, onClick = throttle { onExpandedChange(false) - navController.toDestinationsNavigator().navigate(CategoryPageDestination(subItem.id)) + navController.toDestinationsNavigator() + .navigate(CategoryPageDestination(subItem.id)) } ) } @@ -259,11 +261,23 @@ private fun SubsMenuItem( }, onClick = throttle { onExpandedChange(false) - navController.toDestinationsNavigator().navigate(GlobalRulePageDestination(subItem.id)) + navController.toDestinationsNavigator() + .navigate(GlobalRulePageDestination(subItem.id)) } ) } } + subscription?.supportUri?.let { supportUri -> + DropdownMenuItem( + text = { + Text(text = "问题反馈") + }, + onClick = { + onExpandedChange(false) + context.openUri(supportUri) + } + ) + } DropdownMenuItem( text = { Text(text = "导出数据") @@ -286,15 +300,17 @@ private fun SubsMenuItem( toast("复制成功") } ) - } - subscription?.supportUri?.let { supportUri -> DropdownMenuItem( text = { - Text(text = "问题反馈") + Text(text = "修改链接") }, onClick = { onExpandedChange(false) - context.openUri(supportUri) + vm.viewModelScope.launchTry { + val newUrl = vm.inputSubsLinkOption.getResult(initValue = it) + newUrl ?: return@launchTry + vm.addOrModifySubs(newUrl, subItem) + } } ) } 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 526817a..b62e6d8 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 @@ -1,6 +1,5 @@ package li.songe.gkd.ui.home -import android.webkit.URLUtil import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.blankj.utilcode.util.LogUtils @@ -17,6 +16,7 @@ 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.appInfoCacheFlow import li.songe.gkd.util.clickCountFlow @@ -64,17 +64,12 @@ class HomeVm : ViewModel() { }.stateIn(appScope, SharingStarted.Eagerly, "") } - fun addSubsFromUrl(url: String) = viewModelScope.launchTry(Dispatchers.IO) { + fun addOrModifySubs( + url: String, + oldItem: SubsItem? = null, + ) = viewModelScope.launchTry(Dispatchers.IO) { if (subsRefreshingFlow.value) return@launchTry - if (!URLUtil.isNetworkUrl(url)) { - toast("非法链接") - return@launchTry - } val subItems = subsItemsFlow.value - if (subItems.any { it.updateUrl == url }) { - toast("订阅链接已存在") - return@launchTry - } subsRefreshingFlow.value = true try { val text = try { @@ -93,22 +88,34 @@ class HomeVm : ViewModel() { toast("解析订阅文件失败") return@launchTry } - if (subItems.any { it.id == newSubsRaw.id }) { - 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 = SubsItem( + 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) - DbSet.subsItemDao.insert(newItem) - toast("成功添加订阅") + if (oldItem == null) { + DbSet.subsItemDao.insert(newItem) + toast("成功添加订阅") + } else { + DbSet.subsItemDao.update(newItem) + toast("成功修改订阅") + } } finally { subsRefreshingFlow.value = false } @@ -167,4 +174,6 @@ 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 d85117e..b6a294f 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 @@ -1,7 +1,6 @@ package li.songe.gkd.ui.home import android.content.Intent -import android.webkit.URLUtil import androidx.activity.compose.BackHandler import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -32,7 +31,6 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar @@ -77,7 +75,6 @@ import li.songe.gkd.util.SafeR import li.songe.gkd.util.UpdateTimeOption import li.songe.gkd.util.checkSubsUpdate import li.songe.gkd.util.findOption -import li.songe.gkd.util.isSafeUrl import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchTry import li.songe.gkd.util.map @@ -111,9 +108,6 @@ fun useSubsManagePage(): ScaffoldExt { orderSubItems = subItems } - var showAddLinkDialog by remember { mutableStateOf(false) } - var link by remember { mutableStateOf("") } - val refreshing by subsRefreshingFlow.collectAsState() val pullRefreshState = rememberPullRefreshState(refreshing, { checkSubsUpdate(true) }) var isSelectedMode by remember { mutableStateOf(false) } @@ -135,59 +129,6 @@ fun useSubsManagePage(): ScaffoldExt { } } - LaunchedEffect(showAddLinkDialog) { - if (!showAddLinkDialog) { - link = "" - } - } - if (showAddLinkDialog) { - AlertDialog(title = { Text(text = "添加订阅") }, text = { - OutlinedTextField( - value = link, - onValueChange = { link = it.trim() }, - maxLines = 8, - modifier = Modifier.fillMaxWidth(), - placeholder = { - Text(text = "请输入订阅链接") - }, - isError = link.isNotEmpty() && !URLUtil.isNetworkUrl(link), - ) - }, onDismissRequest = { - if (link.isEmpty()) { - showAddLinkDialog = false - } - }, dismissButton = { - TextButton(onClick = { - showAddLinkDialog = false - }) { - Text(text = "取消") - } - }, confirmButton = { - TextButton(enabled = link.isNotBlank(), onClick = { - if (!URLUtil.isNetworkUrl(link)) { - toast("非法链接") - return@TextButton - } - if (subItems.any { s -> s.updateUrl == link }) { - toast("链接已存在") - return@TextButton - } - vm.viewModelScope.launchTry { - if (!isSafeUrl(link)) { - context.mainVm.dialogFlow.waitResult( - title = "未知来源", - text = "你正在添加一个未验证的远程订阅\n\n这可能含有恶意的规则\n\n是否仍然确认添加?" - ) - } - showAddLinkDialog = false - vm.addSubsFromUrl(url = link) - } - }) { - Text(text = "确认") - } - }) - } - var showSettingsDlg by remember { mutableStateOf(false) } if (showSettingsDlg) { AlertDialog( @@ -212,6 +153,7 @@ fun useSubsManagePage(): ScaffoldExt { } ShareDataDialog(vm) + vm.inputSubsLinkOption.ContentDialog() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() return ScaffoldExt( @@ -389,7 +331,10 @@ fun useSubsManagePage(): ScaffoldExt { toast("正在刷新订阅,请稍后操作") return@FloatingActionButton } - showAddLinkDialog = true + vm.viewModelScope.launchTry { + val url = vm.inputSubsLinkOption.getResult() ?: return@launchTry + vm.addOrModifySubs(url) + } }) { Icon( imageVector = Icons.Filled.Add,