mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 03:32:38 +08:00
feat: support multi-select
This commit is contained in:
parent
9f0f026459
commit
f34b1ca1ea
|
@ -114,6 +114,7 @@ android {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
resValue("string", "app_name", "GKD-debug")
|
resValue("string", "app_name", "GKD-debug")
|
||||||
resValue("string", "capture_label", "捕获快照-debug")
|
resValue("string", "capture_label", "捕获快照-debug")
|
||||||
|
resValue("string", "import_desc", "GKD-debug-导入数据")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import li.songe.gkd.data.RawSubscription
|
||||||
import li.songe.gkd.data.SubsItem
|
import li.songe.gkd.data.SubsItem
|
||||||
import li.songe.gkd.db.DbSet
|
import li.songe.gkd.db.DbSet
|
||||||
import li.songe.gkd.permission.authReasonFlow
|
import li.songe.gkd.permission.authReasonFlow
|
||||||
|
import li.songe.gkd.util.LOCAL_SUBS_ID
|
||||||
import li.songe.gkd.util.checkUpdate
|
import li.songe.gkd.util.checkUpdate
|
||||||
import li.songe.gkd.util.clearCache
|
import li.songe.gkd.util.clearCache
|
||||||
import li.songe.gkd.util.launchTry
|
import li.songe.gkd.util.launchTry
|
||||||
|
@ -24,7 +25,7 @@ class MainViewModel : ViewModel() {
|
||||||
init {
|
init {
|
||||||
|
|
||||||
val localSubsItem = SubsItem(
|
val localSubsItem = SubsItem(
|
||||||
id = -2, order = -2, mtime = System.currentTimeMillis()
|
id = LOCAL_SUBS_ID, order = -2, mtime = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
viewModelScope.launchTry(Dispatchers.IO) {
|
viewModelScope.launchTry(Dispatchers.IO) {
|
||||||
val subsItems = DbSet.subsItemDao.queryAll()
|
val subsItems = DbSet.subsItemDao.queryAll()
|
||||||
|
|
|
@ -40,6 +40,9 @@ data class CategoryConfig(
|
||||||
@Query("DELETE FROM category_config WHERE subs_item_id=:subsItemId")
|
@Query("DELETE FROM category_config WHERE subs_item_id=:subsItemId")
|
||||||
suspend fun deleteBySubsItemId(subsItemId: Long): Int
|
suspend fun deleteBySubsItemId(subsItemId: Long): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM category_config WHERE subs_item_id IN (:subsIds)")
|
||||||
|
suspend fun deleteBySubsId(vararg subsIds: Long): Int
|
||||||
|
|
||||||
@Query("DELETE FROM category_config WHERE subs_item_id=:subsItemId AND category_key=:categoryKey")
|
@Query("DELETE FROM category_config WHERE subs_item_id=:subsItemId AND category_key=:categoryKey")
|
||||||
suspend fun deleteByCategoryKey(subsItemId: Long, categoryKey: Int): Int
|
suspend fun deleteByCategoryKey(subsItemId: Long, categoryKey: Int): Int
|
||||||
|
|
||||||
|
|
|
@ -58,8 +58,8 @@ data class ClickLog(
|
||||||
suspend fun delete(vararg objects: ClickLog): Int
|
suspend fun delete(vararg objects: ClickLog): Int
|
||||||
|
|
||||||
|
|
||||||
@Query("DELETE FROM click_log WHERE subs_id=:subsItemId")
|
@Query("DELETE FROM click_log WHERE subs_id IN (:subsIds)")
|
||||||
suspend fun deleteBySubsId(subsItemId: Long): Int
|
suspend fun deleteBySubsId(vararg subsIds: Long): Int
|
||||||
|
|
||||||
@Query("DELETE FROM click_log")
|
@Query("DELETE FROM click_log")
|
||||||
suspend fun deleteAll()
|
suspend fun deleteAll()
|
||||||
|
|
|
@ -53,6 +53,9 @@ data class SubsConfig(
|
||||||
@Query("DELETE FROM subs_config WHERE subs_item_id=:subsItemId")
|
@Query("DELETE FROM subs_config WHERE subs_item_id=:subsItemId")
|
||||||
suspend fun delete(subsItemId: Long): Int
|
suspend fun delete(subsItemId: Long): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM subs_config WHERE subs_item_id IN (:subsIds)")
|
||||||
|
suspend fun deleteBySubsId(vararg subsIds: Long): Int
|
||||||
|
|
||||||
@Query("DELETE FROM subs_config WHERE subs_item_id=:subsItemId AND app_id=:appId")
|
@Query("DELETE FROM subs_config WHERE subs_item_id=:subsItemId AND app_id=:appId")
|
||||||
suspend fun delete(subsItemId: Long, appId: String): Int
|
suspend fun delete(subsItemId: Long, appId: String): Int
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,16 @@ import androidx.room.PrimaryKey
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import li.songe.gkd.appScope
|
||||||
import li.songe.gkd.db.DbSet
|
import li.songe.gkd.db.DbSet
|
||||||
import li.songe.gkd.util.deleteSubscription
|
|
||||||
import li.songe.gkd.util.isSafeUrl
|
import li.songe.gkd.util.isSafeUrl
|
||||||
|
import li.songe.gkd.util.launchTry
|
||||||
|
import li.songe.gkd.util.subsFolder
|
||||||
|
import li.songe.gkd.util.subsIdToRawFlow
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@Entity(
|
@Entity(
|
||||||
|
@ -50,14 +55,6 @@ data class SubsItem(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeAssets() {
|
|
||||||
deleteSubscription(id)
|
|
||||||
DbSet.subsItemDao.delete(this)
|
|
||||||
DbSet.subsConfigDao.delete(id)
|
|
||||||
DbSet.clickLogDao.deleteBySubsId(id)
|
|
||||||
DbSet.categoryConfigDao.deleteBySubsItemId(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface SubsItemDao {
|
interface SubsItemDao {
|
||||||
@Update
|
@Update
|
||||||
|
@ -94,8 +91,28 @@ data class SubsItem(
|
||||||
@Query("SELECT * FROM subs_item ORDER BY `order`")
|
@Query("SELECT * FROM subs_item ORDER BY `order`")
|
||||||
fun queryAll(): List<SubsItem>
|
fun queryAll(): List<SubsItem>
|
||||||
|
|
||||||
@Query("SELECT * FROM subs_item WHERE id=:id")
|
@Query("DELETE FROM subs_item WHERE id IN (:ids)")
|
||||||
fun queryById(id: Long): Flow<SubsItem?>
|
suspend fun deleteById(vararg ids: Long)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun deleteSubscription(vararg subsIds: Long) {
|
||||||
|
appScope.launchTry(Dispatchers.IO) {
|
||||||
|
DbSet.subsItemDao.deleteById(*subsIds)
|
||||||
|
DbSet.subsConfigDao.deleteBySubsId(*subsIds)
|
||||||
|
DbSet.clickLogDao.deleteBySubsId(*subsIds)
|
||||||
|
DbSet.categoryConfigDao.deleteBySubsId(*subsIds)
|
||||||
|
val newMap = subsIdToRawFlow.value.toMutableMap()
|
||||||
|
subsIds.forEach { id ->
|
||||||
|
newMap.remove(id)
|
||||||
|
subsFolder.resolve("$id.json").apply {
|
||||||
|
if (exists()) {
|
||||||
|
delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subsIdToRawFlow.value = newMap.toImmutableMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package li.songe.gkd.data
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import li.songe.gkd.db.DbSet
|
import li.songe.gkd.db.DbSet
|
||||||
|
import li.songe.gkd.util.LOCAL_SUBS_IDS
|
||||||
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.updateSubscription
|
import li.songe.gkd.util.updateSubscription
|
||||||
|
@ -21,30 +22,30 @@ data class TransferData(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun exportTransferData(subsItemIds: List<Long>): TransferData {
|
suspend fun exportTransferData(subsItemIds: Collection<Long>): TransferData {
|
||||||
return TransferData(
|
return TransferData(
|
||||||
subsItems = subsItemsFlow.value.filter { subsItemIds.contains(it.id) },
|
subsItems = subsItemsFlow.value.filter { subsItemIds.contains(it.id) },
|
||||||
subscriptions = subsIdToRawFlow.value.values.filter { it.id < 0 && subsItemIds.contains(it.id) },
|
subscriptions = subsIdToRawFlow.value.values.filter { it.id < 0 && subsItemIds.contains(it.id) },
|
||||||
subsConfigs = DbSet.subsConfigDao.querySubsItemConfig(subsItemIds),
|
subsConfigs = DbSet.subsConfigDao.querySubsItemConfig(subsItemIds.toList()),
|
||||||
categoryConfigs = DbSet.categoryConfigDao.querySubsItemConfig(subsItemIds),
|
categoryConfigs = DbSet.categoryConfigDao.querySubsItemConfig(subsItemIds.toList()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun importTransferData(transferData: TransferData): Boolean {
|
suspend fun importTransferData(transferData: TransferData): Boolean {
|
||||||
// TODO transaction
|
// TODO transaction
|
||||||
val localIds = arrayOf(-1L, -2L)
|
|
||||||
val maxOrder = (subsItemsFlow.value.maxOfOrNull { it.order } ?: -1) + 1
|
val maxOrder = (subsItemsFlow.value.maxOfOrNull { it.order } ?: -1) + 1
|
||||||
val subsItems = transferData.subsItems.filter { s -> s.id >= 0 || localIds.contains(s.id) }
|
val subsItems =
|
||||||
.mapIndexed { i, s ->
|
transferData.subsItems.filter { s -> s.id >= 0 || LOCAL_SUBS_IDS.contains(s.id) }
|
||||||
s.copy(order = maxOrder + i)
|
.mapIndexed { i, s ->
|
||||||
}
|
s.copy(order = maxOrder + i)
|
||||||
|
}
|
||||||
val hasNewSubsItem =
|
val hasNewSubsItem =
|
||||||
subsItems.any { newSubs -> newSubs.id >= 0 && subsItemsFlow.value.all { oldSubs -> oldSubs.id != newSubs.id } }
|
subsItems.any { newSubs -> newSubs.id >= 0 && subsItemsFlow.value.all { oldSubs -> oldSubs.id != newSubs.id } }
|
||||||
DbSet.subsItemDao.insertOrIgnore(*subsItems.toTypedArray())
|
DbSet.subsItemDao.insertOrIgnore(*subsItems.toTypedArray())
|
||||||
DbSet.subsConfigDao.insertOrIgnore(*transferData.subsConfigs.toTypedArray())
|
DbSet.subsConfigDao.insertOrIgnore(*transferData.subsConfigs.toTypedArray())
|
||||||
DbSet.categoryConfigDao.insertOrIgnore(*transferData.categoryConfigs.toTypedArray())
|
DbSet.categoryConfigDao.insertOrIgnore(*transferData.categoryConfigs.toTypedArray())
|
||||||
transferData.subscriptions.forEach { subscription ->
|
transferData.subscriptions.forEach { subscription ->
|
||||||
if (localIds.contains(subscription.id)) {
|
if (LOCAL_SUBS_IDS.contains(subscription.id)) {
|
||||||
updateSubscription(subscription)
|
updateSubscription(subscription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,12 +35,14 @@ import li.songe.gkd.data.GkdAction
|
||||||
import li.songe.gkd.data.RawSubscription
|
import li.songe.gkd.data.RawSubscription
|
||||||
import li.songe.gkd.data.RpcError
|
import li.songe.gkd.data.RpcError
|
||||||
import li.songe.gkd.data.SubsItem
|
import li.songe.gkd.data.SubsItem
|
||||||
|
import li.songe.gkd.data.deleteSubscription
|
||||||
import li.songe.gkd.db.DbSet
|
import li.songe.gkd.db.DbSet
|
||||||
import li.songe.gkd.debug.SnapshotExt.captureSnapshot
|
import li.songe.gkd.debug.SnapshotExt.captureSnapshot
|
||||||
import li.songe.gkd.notif.createNotif
|
import li.songe.gkd.notif.createNotif
|
||||||
import li.songe.gkd.notif.httpChannel
|
import li.songe.gkd.notif.httpChannel
|
||||||
import li.songe.gkd.notif.httpNotif
|
import li.songe.gkd.notif.httpNotif
|
||||||
import li.songe.gkd.service.GkdAbService
|
import li.songe.gkd.service.GkdAbService
|
||||||
|
import li.songe.gkd.util.LOCAL_HTTP_SUBS_ID
|
||||||
import li.songe.gkd.util.SERVER_SCRIPT_URL
|
import li.songe.gkd.util.SERVER_SCRIPT_URL
|
||||||
import li.songe.gkd.util.getIpAddressInLocalNetwork
|
import li.songe.gkd.util.getIpAddressInLocalNetwork
|
||||||
import li.songe.gkd.util.keepNullJson
|
import li.songe.gkd.util.keepNullJson
|
||||||
|
@ -58,7 +60,7 @@ class HttpService : CompositionService({
|
||||||
val scope = CoroutineScope(Dispatchers.IO)
|
val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
val httpSubsItem = SubsItem(
|
val httpSubsItem = SubsItem(
|
||||||
id = -1L,
|
id = LOCAL_HTTP_SUBS_ID,
|
||||||
order = -1,
|
order = -1,
|
||||||
enableUpdate = false,
|
enableUpdate = false,
|
||||||
)
|
)
|
||||||
|
@ -101,7 +103,7 @@ class HttpService : CompositionService({
|
||||||
val subscription =
|
val subscription =
|
||||||
RawSubscription.parse(call.receiveText(), json5 = false)
|
RawSubscription.parse(call.receiveText(), json5 = false)
|
||||||
.copy(
|
.copy(
|
||||||
id = -1,
|
id = LOCAL_HTTP_SUBS_ID,
|
||||||
name = "内存订阅",
|
name = "内存订阅",
|
||||||
version = 0,
|
version = 0,
|
||||||
author = "@gkd-kit/inspect"
|
author = "@gkd-kit/inspect"
|
||||||
|
@ -155,7 +157,7 @@ class HttpService : CompositionService({
|
||||||
scope.launchTry(Dispatchers.IO) {
|
scope.launchTry(Dispatchers.IO) {
|
||||||
server?.stop()
|
server?.stop()
|
||||||
if (storeFlow.value.autoClearMemorySubs) {
|
if (storeFlow.value.autoClearMemorySubs) {
|
||||||
httpSubsItem.removeAssets()
|
deleteSubscription(LOCAL_HTTP_SUBS_ID)
|
||||||
}
|
}
|
||||||
delay(3000)
|
delay(3000)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
|
@ -194,11 +196,7 @@ fun clearHttpSubs() {
|
||||||
appScope.launchTry(Dispatchers.IO) {
|
appScope.launchTry(Dispatchers.IO) {
|
||||||
delay(1000)
|
delay(1000)
|
||||||
if (storeFlow.value.autoClearMemorySubs) {
|
if (storeFlow.value.autoClearMemorySubs) {
|
||||||
SubsItem(
|
deleteSubscription(LOCAL_HTTP_SUBS_ID)
|
||||||
id = -1L,
|
|
||||||
order = -1,
|
|
||||||
enableUpdate = false,
|
|
||||||
).removeAssets()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -58,6 +58,7 @@ import li.songe.gkd.db.DbSet
|
||||||
import li.songe.gkd.ui.destinations.AppItemPageDestination
|
import li.songe.gkd.ui.destinations.AppItemPageDestination
|
||||||
import li.songe.gkd.ui.destinations.GlobalRulePageDestination
|
import li.songe.gkd.ui.destinations.GlobalRulePageDestination
|
||||||
import li.songe.gkd.ui.style.itemPadding
|
import li.songe.gkd.ui.style.itemPadding
|
||||||
|
import li.songe.gkd.util.LOCAL_SUBS_ID
|
||||||
import li.songe.gkd.util.LocalNavController
|
import li.songe.gkd.util.LocalNavController
|
||||||
import li.songe.gkd.util.ProfileTransitions
|
import li.songe.gkd.util.ProfileTransitions
|
||||||
import li.songe.gkd.util.RuleSortOption
|
import li.songe.gkd.util.RuleSortOption
|
||||||
|
@ -150,7 +151,7 @@ fun AppConfigPage(appId: String) {
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate(AppItemPageDestination(-2, appId))
|
navController.navigate(AppItemPageDestination(LOCAL_SUBS_ID, appId))
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package li.songe.gkd.ui.component
|
package li.songe.gkd.ui.component
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||||
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
|
||||||
|
@ -8,6 +10,7 @@ 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.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
@ -36,10 +39,12 @@ 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.TransferData
|
||||||
|
import li.songe.gkd.data.deleteSubscription
|
||||||
import li.songe.gkd.data.exportTransferData
|
import li.songe.gkd.data.exportTransferData
|
||||||
import li.songe.gkd.ui.destinations.CategoryPageDestination
|
import li.songe.gkd.ui.destinations.CategoryPageDestination
|
||||||
import li.songe.gkd.ui.destinations.GlobalRulePageDestination
|
import li.songe.gkd.ui.destinations.GlobalRulePageDestination
|
||||||
import li.songe.gkd.ui.destinations.SubsPageDestination
|
import li.songe.gkd.ui.destinations.SubsPageDestination
|
||||||
|
import li.songe.gkd.util.LOCAL_SUBS_ID
|
||||||
import li.songe.gkd.util.LocalNavController
|
import li.songe.gkd.util.LocalNavController
|
||||||
import li.songe.gkd.util.exportZipDir
|
import li.songe.gkd.util.exportZipDir
|
||||||
import li.songe.gkd.util.formatTimeAgo
|
import li.songe.gkd.util.formatTimeAgo
|
||||||
|
@ -63,7 +68,10 @@ fun SubsItemCard(
|
||||||
subscription: RawSubscription?,
|
subscription: RawSubscription?,
|
||||||
index: Int,
|
index: Int,
|
||||||
vm: ViewModel,
|
vm: ViewModel,
|
||||||
|
isSelectedMode: Boolean,
|
||||||
|
isSelected: Boolean,
|
||||||
onCheckedChange: ((Boolean) -> Unit)? = null,
|
onCheckedChange: ((Boolean) -> Unit)? = null,
|
||||||
|
onSelectedChange: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val subsLoadError by remember(subsItem.id) {
|
val subsLoadError by remember(subsItem.id) {
|
||||||
|
@ -74,15 +82,28 @@ fun SubsItemCard(
|
||||||
}.collectAsState()
|
}.collectAsState()
|
||||||
val subsRefreshing by subsRefreshingFlow.collectAsState()
|
val subsRefreshing by subsRefreshingFlow.collectAsState()
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
Card(
|
val dragged by interactionSource.collectIsDraggedAsState()
|
||||||
onClick = {
|
val onClick = {
|
||||||
if (!subsRefreshingFlow.value) {
|
if (!dragged) {
|
||||||
|
if (isSelectedMode) {
|
||||||
|
onSelectedChange?.invoke()
|
||||||
|
} else if (!subsRefreshingFlow.value) {
|
||||||
expanded = true
|
expanded = true
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
Card(
|
||||||
|
onClick = onClick,
|
||||||
modifier = modifier.padding(16.dp, 2.dp),
|
modifier = modifier.padding(16.dp, 2.dp),
|
||||||
shape = MaterialTheme.shapes.small,
|
shape = MaterialTheme.shapes.small,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = if (isSelected) {
|
||||||
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
|
} else {
|
||||||
|
Color.Unspecified
|
||||||
|
}
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
SubsMenuItem(
|
SubsMenuItem(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
|
@ -165,7 +186,8 @@ fun SubsItemCard(
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
Switch(
|
Switch(
|
||||||
checked = subsItem.enable,
|
checked = subsItem.enable,
|
||||||
onCheckedChange = onCheckedChange,
|
enabled = !isSelectedMode,
|
||||||
|
onCheckedChange = if (isSelectedMode) null else onCheckedChange,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -222,28 +244,12 @@ private fun SubsMenuItem(
|
||||||
}
|
}
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text(text = "导出数据")
|
Text(text = "分享数据")
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
onExpandedChange(false)
|
onExpandedChange(false)
|
||||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||||
val transferDataFile = exportZipDir.resolve("${TransferData.TYPE}.json")
|
context.shareSubs(subItem.id)
|
||||||
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, "分享数据文件")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -270,7 +276,7 @@ private fun SubsMenuItem(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (subItem.id != -2L) {
|
if (subItem.id != LOCAL_SUBS_ID) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text(text = "删除订阅", color = MaterialTheme.colorScheme.error)
|
Text(text = "删除订阅", color = MaterialTheme.colorScheme.error)
|
||||||
|
@ -283,10 +289,21 @@ private fun SubsMenuItem(
|
||||||
"是否删除订阅 ${subscription?.name ?: subItem.id} ?",
|
"是否删除订阅 ${subscription?.name ?: subItem.id} ?",
|
||||||
)
|
)
|
||||||
if (!result) return@launchTry
|
if (!result) return@launchTry
|
||||||
subItem.removeAssets()
|
deleteSubscription(subItem.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun Context.shareSubs(vararg subsIds: Long) {
|
||||||
|
val transferDataFile = exportZipDir.resolve("${TransferData.TYPE}.json")
|
||||||
|
transferDataFile.writeText(
|
||||||
|
json.encodeToString(exportTransferData(subsIds.toList()))
|
||||||
|
)
|
||||||
|
val file = exportZipDir.resolve("backup-${System.currentTimeMillis()}.zip")
|
||||||
|
ZipUtils.zipFiles(listOf(transferDataFile), file)
|
||||||
|
transferDataFile.delete()
|
||||||
|
this.shareFile(file, "分享数据文件")
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package li.songe.gkd.ui.home
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.webkit.URLUtil
|
import android.webkit.URLUtil
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
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
|
||||||
|
@ -18,7 +19,10 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
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.Close
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
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.AlertDialog
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
@ -44,6 +48,7 @@ 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.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
@ -54,10 +59,14 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import li.songe.gkd.data.TransferData
|
import li.songe.gkd.data.TransferData
|
||||||
|
import li.songe.gkd.data.Value
|
||||||
|
import li.songe.gkd.data.deleteSubscription
|
||||||
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.component.shareSubs
|
||||||
|
import li.songe.gkd.util.LOCAL_SUBS_ID
|
||||||
import li.songe.gkd.util.LocalLauncher
|
import li.songe.gkd.util.LocalLauncher
|
||||||
import li.songe.gkd.util.checkSubsUpdate
|
import li.songe.gkd.util.checkSubsUpdate
|
||||||
import li.songe.gkd.util.isSafeUrl
|
import li.songe.gkd.util.isSafeUrl
|
||||||
|
@ -79,6 +88,7 @@ val subsNav = BottomNavItem(
|
||||||
@Composable
|
@Composable
|
||||||
fun useSubsManagePage(): ScaffoldExt {
|
fun useSubsManagePage(): ScaffoldExt {
|
||||||
val launcher = LocalLauncher.current
|
val launcher = LocalLauncher.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val vm = hiltViewModel<HomeVm>()
|
val vm = hiltViewModel<HomeVm>()
|
||||||
val subItems by subsItemsFlow.collectAsState()
|
val subItems by subsItemsFlow.collectAsState()
|
||||||
|
@ -96,6 +106,24 @@ fun useSubsManagePage(): ScaffoldExt {
|
||||||
|
|
||||||
val refreshing by subsRefreshingFlow.collectAsState()
|
val refreshing by subsRefreshingFlow.collectAsState()
|
||||||
val pullRefreshState = rememberPullRefreshState(refreshing, { checkSubsUpdate(true) })
|
val pullRefreshState = rememberPullRefreshState(refreshing, { checkSubsUpdate(true) })
|
||||||
|
var isSelectedMode by remember { mutableStateOf(false) }
|
||||||
|
var selectedIds by remember { mutableStateOf(emptySet<Long>()) }
|
||||||
|
val draggedFlag = remember { Value(false) }
|
||||||
|
LaunchedEffect(key1 = isSelectedMode) {
|
||||||
|
if (!isSelectedMode && selectedIds.isNotEmpty()) {
|
||||||
|
selectedIds = emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isSelectedMode) {
|
||||||
|
BackHandler {
|
||||||
|
isSelectedMode = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(key1 = subItems.size) {
|
||||||
|
if (subItems.size <= 1) {
|
||||||
|
isSelectedMode = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(showAddLinkDialog) {
|
LaunchedEffect(showAddLinkDialog) {
|
||||||
if (!showAddLinkDialog) {
|
if (!showAddLinkDialog) {
|
||||||
|
@ -119,53 +147,92 @@ fun useSubsManagePage(): ScaffoldExt {
|
||||||
isError = link.isNotEmpty() && !URLUtil.isNetworkUrl(link),
|
isError = link.isNotEmpty() && !URLUtil.isNetworkUrl(link),
|
||||||
)
|
)
|
||||||
}, onDismissRequest = { showAddLinkDialog = false }, confirmButton = {
|
}, onDismissRequest = { showAddLinkDialog = false }, confirmButton = {
|
||||||
TextButton(
|
TextButton(enabled = link.isNotBlank(), onClick = {
|
||||||
enabled = link.isNotBlank(),
|
if (!URLUtil.isNetworkUrl(link)) {
|
||||||
onClick = {
|
toast("非法链接")
|
||||||
if (!URLUtil.isNetworkUrl(link)) {
|
return@TextButton
|
||||||
toast("非法链接")
|
}
|
||||||
return@TextButton
|
if (subItems.any { s -> s.updateUrl == link }) {
|
||||||
|
toast("链接已存在")
|
||||||
|
return@TextButton
|
||||||
|
}
|
||||||
|
vm.viewModelScope.launchTry {
|
||||||
|
if (!isSafeUrl(link)) {
|
||||||
|
val result = getDialogResult(
|
||||||
|
"未知来源",
|
||||||
|
"你正在添加一个未验证的远程订阅\n\n这可能含有恶意的规则\n\n是否仍然确认添加?"
|
||||||
|
)
|
||||||
|
if (!result) return@launchTry
|
||||||
}
|
}
|
||||||
if (subItems.any { s -> s.updateUrl == link }) {
|
showAddLinkDialog = false
|
||||||
toast("链接已存在")
|
vm.addSubsFromUrl(url = link)
|
||||||
return@TextButton
|
}
|
||||||
}
|
}) {
|
||||||
vm.viewModelScope.launchTry {
|
|
||||||
if (!isSafeUrl(link)) {
|
|
||||||
val result = getDialogResult(
|
|
||||||
"未知来源",
|
|
||||||
"你正在添加一个未验证的远程订阅\n\n这可能含有恶意的规则\n\n是否仍然确认添加?"
|
|
||||||
)
|
|
||||||
if (!result) return@launchTry
|
|
||||||
}
|
|
||||||
showAddLinkDialog = false
|
|
||||||
vm.addSubsFromUrl(url = link)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Text(text = "添加")
|
Text(text = "添加")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
return ScaffoldExt(
|
return ScaffoldExt(
|
||||||
navItem = subsNav,
|
navItem = subsNav,
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(scrollBehavior = scrollBehavior, navigationIcon = {
|
||||||
scrollBehavior = scrollBehavior,
|
if (isSelectedMode) {
|
||||||
title = {
|
IconButton(onClick = { isSelectedMode = false }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, title = {
|
||||||
|
if (isSelectedMode) {
|
||||||
|
Text(
|
||||||
|
text = if (selectedIds.isNotEmpty()) selectedIds.size.toString() else "",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = subsNav.label,
|
text = subsNav.label,
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
actions = {
|
}, actions = {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
IconButton(onClick = {
|
if (isSelectedMode) {
|
||||||
if (subsRefreshingFlow.value) {
|
val canDeleteIds = if (selectedIds.contains(LOCAL_SUBS_ID)) {
|
||||||
toast("正在刷新订阅,请稍后操作")
|
selectedIds - LOCAL_SUBS_ID
|
||||||
return@IconButton
|
} else {
|
||||||
|
selectedIds
|
||||||
|
}
|
||||||
|
if (canDeleteIds.isNotEmpty()) {
|
||||||
|
IconButton(onClick = vm.viewModelScope.launchAsFn {
|
||||||
|
val result = getDialogResult(
|
||||||
|
"删除订阅",
|
||||||
|
"是否删除所选 ${canDeleteIds.size} 个订阅?\n\n注: 不包含本地订阅",
|
||||||
|
)
|
||||||
|
if (!result) return@launchAsFn
|
||||||
|
deleteSubscription(*canDeleteIds.toLongArray())
|
||||||
|
selectedIds = selectedIds - canDeleteIds
|
||||||
|
if (selectedIds.size == canDeleteIds.size) {
|
||||||
|
isSelectedMode = false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Delete,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
IconButton(onClick = vm.viewModelScope.launchAsFn(Dispatchers.IO) {
|
||||||
|
context.shareSubs(*selectedIds.toLongArray())
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Share,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = {
|
||||||
expanded = true
|
expanded = true
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -173,14 +240,49 @@ fun useSubsManagePage(): ScaffoldExt {
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(
|
} else {
|
||||||
modifier = Modifier
|
IconButton(onClick = {
|
||||||
.wrapContentSize(Alignment.TopStart)
|
if (subsRefreshingFlow.value) {
|
||||||
) {
|
toast("正在刷新订阅,请稍后操作")
|
||||||
DropdownMenu(
|
} else {
|
||||||
expanded = expanded,
|
expanded = true
|
||||||
onDismissRequest = { expanded = false }
|
}
|
||||||
) {
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MoreVert,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.wrapContentSize(Alignment.TopStart)
|
||||||
|
) {
|
||||||
|
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
if (isSelectedMode) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(text = "全选")
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
selectedIds = subItems.map { it.id }.toSet()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(text = "反选")
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
val newSelectedIds =
|
||||||
|
subItems.map { it.id }.toSet() - selectedIds
|
||||||
|
if (newSelectedIds.isEmpty()) {
|
||||||
|
isSelectedMode = false
|
||||||
|
}
|
||||||
|
selectedIds = newSelectedIds
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -204,8 +306,7 @@ fun useSubsManagePage(): ScaffoldExt {
|
||||||
return@launchAsFn
|
return@launchAsFn
|
||||||
}
|
}
|
||||||
val string = readFileZipByteArray(
|
val string = readFileZipByteArray(
|
||||||
UriUtils.uri2Bytes(uri),
|
UriUtils.uri2Bytes(uri), "${TransferData.TYPE}.json"
|
||||||
"${TransferData.TYPE}.json"
|
|
||||||
)
|
)
|
||||||
if (string != null) {
|
if (string != null) {
|
||||||
val transferData =
|
val transferData =
|
||||||
|
@ -220,20 +321,22 @@ fun useSubsManagePage(): ScaffoldExt {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(onClick = {
|
if (!isSelectedMode) {
|
||||||
if (subsRefreshingFlow.value) {
|
FloatingActionButton(onClick = {
|
||||||
toast("正在刷新订阅,请稍后操作")
|
if (subsRefreshingFlow.value) {
|
||||||
return@FloatingActionButton
|
toast("正在刷新订阅,请稍后操作")
|
||||||
|
return@FloatingActionButton
|
||||||
|
}
|
||||||
|
showAddLinkDialog = true
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Add,
|
||||||
|
contentDescription = "info",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
showAddLinkDialog = true
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Add,
|
|
||||||
contentDescription = "info",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
|
@ -248,6 +351,7 @@ fun useSubsManagePage(): ScaffoldExt {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.toImmutableList()
|
}.toImmutableList()
|
||||||
|
draggedFlag.value = true
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -261,38 +365,64 @@ fun useSubsManagePage(): ScaffoldExt {
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(orderSubItems, { _, subItem -> subItem.id }) { index, subItem ->
|
itemsIndexed(orderSubItems, { _, subItem -> subItem.id }) { index, subItem ->
|
||||||
|
val canDrag = !refreshing && orderSubItems.size > 1
|
||||||
ReorderableItem(
|
ReorderableItem(
|
||||||
reorderableLazyColumnState,
|
reorderableLazyColumnState,
|
||||||
key = subItem.id,
|
key = subItem.id,
|
||||||
enabled = !refreshing,
|
enabled = canDrag,
|
||||||
) {
|
) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
SubsItemCard(
|
SubsItemCard(
|
||||||
modifier = Modifier
|
modifier = Modifier.longPressDraggableHandle(
|
||||||
.longPressDraggableHandle(
|
enabled = canDrag,
|
||||||
enabled = !refreshing,
|
interactionSource = interactionSource,
|
||||||
interactionSource = interactionSource,
|
onDragStarted = {
|
||||||
onDragStopped = {
|
if (orderSubItems.size > 1 && !isSelectedMode) {
|
||||||
val changeItems = orderSubItems.filter { newItem ->
|
isSelectedMode = true
|
||||||
subItems.find { oldItem -> oldItem.id == newItem.id }?.order != newItem.order
|
selectedIds = setOf(subItem.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragStopped = {
|
||||||
|
if (draggedFlag.value) {
|
||||||
|
draggedFlag.value = false
|
||||||
|
isSelectedMode = false
|
||||||
|
selectedIds = emptySet()
|
||||||
|
}
|
||||||
|
val changeItems = orderSubItems.filter { newItem ->
|
||||||
|
subItems.find { oldItem -> oldItem.id == newItem.id }?.order != newItem.order
|
||||||
|
}
|
||||||
|
if (changeItems.isNotEmpty()) {
|
||||||
|
vm.viewModelScope.launchTry {
|
||||||
|
DbSet.subsItemDao.batchUpdateOrder(changeItems)
|
||||||
}
|
}
|
||||||
if (changeItems.isNotEmpty()) {
|
}
|
||||||
vm.viewModelScope.launchTry {
|
},
|
||||||
DbSet.subsItemDao.batchUpdateOrder(changeItems)
|
),
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
subsItem = subItem,
|
subsItem = subItem,
|
||||||
subscription = subsIdToRaw[subItem.id],
|
subscription = subsIdToRaw[subItem.id],
|
||||||
index = index + 1,
|
index = index + 1,
|
||||||
vm = vm,
|
vm = vm,
|
||||||
|
isSelectedMode = isSelectedMode,
|
||||||
|
isSelected = selectedIds.contains(subItem.id),
|
||||||
onCheckedChange = { checked ->
|
onCheckedChange = { checked ->
|
||||||
vm.viewModelScope.launch {
|
vm.viewModelScope.launch {
|
||||||
DbSet.subsItemDao.updateEnable(subItem.id, checked)
|
DbSet.subsItemDao.updateEnable(subItem.id, checked)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSelectedChange = {
|
||||||
|
val newSelectedIds = if (selectedIds.contains(subItem.id)) {
|
||||||
|
selectedIds.toMutableSet().apply {
|
||||||
|
remove(subItem.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedIds + subItem.id
|
||||||
|
}
|
||||||
|
selectedIds = newSelectedIds
|
||||||
|
if (newSelectedIds.isEmpty()) {
|
||||||
|
isSelectedMode = false
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,8 @@ private val safeRemoteBaseUrls = arrayOf(
|
||||||
fun isSafeUrl(url: String): Boolean {
|
fun isSafeUrl(url: String): Boolean {
|
||||||
if (!URLUtil.isHttpsUrl(url)) return false
|
if (!URLUtil.isHttpsUrl(url)) return false
|
||||||
return safeRemoteBaseUrls.any { u -> url.startsWith(u) }
|
return safeRemoteBaseUrls.any { u -> url.startsWith(u) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val LOCAL_SUBS_ID = -2L
|
||||||
|
const val LOCAL_HTTP_SUBS_ID = -1L
|
||||||
|
val LOCAL_SUBS_IDS = arrayOf(LOCAL_SUBS_ID, LOCAL_HTTP_SUBS_ID)
|
||||||
|
|
|
@ -102,17 +102,6 @@ fun updateSubscription(subscription: RawSubscription) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteSubscription(subsId: Long) {
|
|
||||||
val newMap = subsIdToRawFlow.value.toMutableMap()
|
|
||||||
newMap.remove(subsId)
|
|
||||||
subsIdToRawFlow.value = newMap.toImmutableMap()
|
|
||||||
subsFolder.resolve("$subsId.json").apply {
|
|
||||||
if (exists()) {
|
|
||||||
delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGroupRawEnable(
|
fun getGroupRawEnable(
|
||||||
group: RawSubscription.RawGroupProps,
|
group: RawSubscription.RawGroupProps,
|
||||||
subsConfig: SubsConfig?,
|
subsConfig: SubsConfig?,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user