Skip to content

Commit

Permalink
feat: support multi-select
Browse files Browse the repository at this point in the history
  • Loading branch information
lisonge committed Jun 9, 2024
1 parent 9f0f026 commit f34b1ca
Show file tree
Hide file tree
Showing 13 changed files with 301 additions and 136 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ android {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "GKD-debug")
resValue("string", "capture_label", "捕获快照-debug")
resValue("string", "import_desc", "GKD-debug-导入数据")
}
}
compileOptions {
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/kotlin/li/songe/gkd/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.SubsItem
import li.songe.gkd.db.DbSet
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.clearCache
import li.songe.gkd.util.launchTry
Expand All @@ -24,7 +25,7 @@ class MainViewModel : ViewModel() {
init {

val localSubsItem = SubsItem(
id = -2, order = -2, mtime = System.currentTimeMillis()
id = LOCAL_SUBS_ID, order = -2, mtime = System.currentTimeMillis()
)
viewModelScope.launchTry(Dispatchers.IO) {
val subsItems = DbSet.subsItemDao.queryAll()
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/kotlin/li/songe/gkd/data/CategoryConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ data class CategoryConfig(
@Query("DELETE FROM category_config WHERE subs_item_id=:subsItemId")
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")
suspend fun deleteByCategoryKey(subsItemId: Long, categoryKey: Int): Int

Expand Down
4 changes: 2 additions & 2 deletions app/src/main/kotlin/li/songe/gkd/data/ClickLog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ data class ClickLog(
suspend fun delete(vararg objects: ClickLog): Int


@Query("DELETE FROM click_log WHERE subs_id=:subsItemId")
suspend fun deleteBySubsId(subsItemId: Long): Int
@Query("DELETE FROM click_log WHERE subs_id IN (:subsIds)")
suspend fun deleteBySubsId(vararg subsIds: Long): Int

@Query("DELETE FROM click_log")
suspend fun deleteAll()
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/kotlin/li/songe/gkd/data/SubsConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ data class SubsConfig(
@Query("DELETE FROM subs_config WHERE subs_item_id=:subsItemId")
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")
suspend fun delete(subsItemId: Long, appId: String): Int

Expand Down
39 changes: 28 additions & 11 deletions app/src/main/kotlin/li/songe/gkd/data/SubsItem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import li.songe.gkd.appScope
import li.songe.gkd.db.DbSet
import li.songe.gkd.util.deleteSubscription
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
@Entity(
Expand Down Expand Up @@ -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
interface SubsItemDao {
@Update
Expand Down Expand Up @@ -94,8 +91,28 @@ data class SubsItem(
@Query("SELECT * FROM subs_item ORDER BY `order`")
fun queryAll(): List<SubsItem>

@Query("SELECT * FROM subs_item WHERE id=:id")
fun queryById(id: Long): Flow<SubsItem?>
@Query("DELETE FROM subs_item WHERE id IN (:ids)")
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()
}
}
19 changes: 10 additions & 9 deletions app/src/main/kotlin/li/songe/gkd/data/TransferData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package li.songe.gkd.data

import kotlinx.serialization.Serializable
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.subsItemsFlow
import li.songe.gkd.util.updateSubscription
Expand All @@ -21,30 +22,30 @@ data class TransferData(
}
}

suspend fun exportTransferData(subsItemIds: List<Long>): TransferData {
suspend fun exportTransferData(subsItemIds: Collection<Long>): TransferData {
return TransferData(
subsItems = subsItemsFlow.value.filter { subsItemIds.contains(it.id) },
subscriptions = subsIdToRawFlow.value.values.filter { it.id < 0 && subsItemIds.contains(it.id) },
subsConfigs = DbSet.subsConfigDao.querySubsItemConfig(subsItemIds),
categoryConfigs = DbSet.categoryConfigDao.querySubsItemConfig(subsItemIds),
subsConfigs = DbSet.subsConfigDao.querySubsItemConfig(subsItemIds.toList()),
categoryConfigs = DbSet.categoryConfigDao.querySubsItemConfig(subsItemIds.toList()),
)
}

suspend fun importTransferData(transferData: TransferData): Boolean {
// TODO transaction
val localIds = arrayOf(-1L, -2L)
val maxOrder = (subsItemsFlow.value.maxOfOrNull { it.order } ?: -1) + 1
val subsItems = transferData.subsItems.filter { s -> s.id >= 0 || localIds.contains(s.id) }
.mapIndexed { i, s ->
s.copy(order = maxOrder + i)
}
val subsItems =
transferData.subsItems.filter { s -> s.id >= 0 || LOCAL_SUBS_IDS.contains(s.id) }
.mapIndexed { i, s ->
s.copy(order = maxOrder + i)
}
val hasNewSubsItem =
subsItems.any { newSubs -> newSubs.id >= 0 && subsItemsFlow.value.all { oldSubs -> oldSubs.id != newSubs.id } }
DbSet.subsItemDao.insertOrIgnore(*subsItems.toTypedArray())
DbSet.subsConfigDao.insertOrIgnore(*transferData.subsConfigs.toTypedArray())
DbSet.categoryConfigDao.insertOrIgnore(*transferData.categoryConfigs.toTypedArray())
transferData.subscriptions.forEach { subscription ->
if (localIds.contains(subscription.id)) {
if (LOCAL_SUBS_IDS.contains(subscription.id)) {
updateSubscription(subscription)
}
}
Expand Down
14 changes: 6 additions & 8 deletions app/src/main/kotlin/li/songe/gkd/debug/HttpService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ import li.songe.gkd.data.GkdAction
import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.RpcError
import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.deleteSubscription
import li.songe.gkd.db.DbSet
import li.songe.gkd.debug.SnapshotExt.captureSnapshot
import li.songe.gkd.notif.createNotif
import li.songe.gkd.notif.httpChannel
import li.songe.gkd.notif.httpNotif
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.getIpAddressInLocalNetwork
import li.songe.gkd.util.keepNullJson
Expand All @@ -58,7 +60,7 @@ class HttpService : CompositionService({
val scope = CoroutineScope(Dispatchers.IO)

val httpSubsItem = SubsItem(
id = -1L,
id = LOCAL_HTTP_SUBS_ID,
order = -1,
enableUpdate = false,
)
Expand Down Expand Up @@ -101,7 +103,7 @@ class HttpService : CompositionService({
val subscription =
RawSubscription.parse(call.receiveText(), json5 = false)
.copy(
id = -1,
id = LOCAL_HTTP_SUBS_ID,
name = "内存订阅",
version = 0,
author = "@gkd-kit/inspect"
Expand Down Expand Up @@ -155,7 +157,7 @@ class HttpService : CompositionService({
scope.launchTry(Dispatchers.IO) {
server?.stop()
if (storeFlow.value.autoClearMemorySubs) {
httpSubsItem.removeAssets()
deleteSubscription(LOCAL_HTTP_SUBS_ID)
}
delay(3000)
scope.cancel()
Expand Down Expand Up @@ -194,11 +196,7 @@ fun clearHttpSubs() {
appScope.launchTry(Dispatchers.IO) {
delay(1000)
if (storeFlow.value.autoClearMemorySubs) {
SubsItem(
id = -1L,
order = -1,
enableUpdate = false,
).removeAssets()
deleteSubscription(LOCAL_HTTP_SUBS_ID)
}
}
}
3 changes: 2 additions & 1 deletion app/src/main/kotlin/li/songe/gkd/ui/AppConfigPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import li.songe.gkd.db.DbSet
import li.songe.gkd.ui.destinations.AppItemPageDestination
import li.songe.gkd.ui.destinations.GlobalRulePageDestination
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.ProfileTransitions
import li.songe.gkd.util.RuleSortOption
Expand Down Expand Up @@ -150,7 +151,7 @@ fun AppConfigPage(appId: String) {
floatingActionButton = {
FloatingActionButton(
onClick = {
navController.navigate(AppItemPageDestination(-2, appId))
navController.navigate(AppItemPageDestination(LOCAL_SUBS_ID, appId))
},
content = {
Icon(
Expand Down
67 changes: 42 additions & 25 deletions app/src/main/kotlin/li/songe/gkd/ui/component/SubsItemCard.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package li.songe.gkd.ui.component

import android.content.Context
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.LocalContentColor
Expand Down Expand Up @@ -36,10 +39,12 @@ import kotlinx.serialization.encodeToString
import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.TransferData
import li.songe.gkd.data.deleteSubscription
import li.songe.gkd.data.exportTransferData
import li.songe.gkd.ui.destinations.CategoryPageDestination
import li.songe.gkd.ui.destinations.GlobalRulePageDestination
import li.songe.gkd.ui.destinations.SubsPageDestination
import li.songe.gkd.util.LOCAL_SUBS_ID
import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.exportZipDir
import li.songe.gkd.util.formatTimeAgo
Expand All @@ -63,7 +68,10 @@ fun SubsItemCard(
subscription: RawSubscription?,
index: Int,
vm: ViewModel,
isSelectedMode: Boolean,
isSelected: Boolean,
onCheckedChange: ((Boolean) -> Unit)? = null,
onSelectedChange: (() -> Unit)? = null,
) {
val scope = rememberCoroutineScope()
val subsLoadError by remember(subsItem.id) {
Expand All @@ -74,15 +82,28 @@ fun SubsItemCard(
}.collectAsState()
val subsRefreshing by subsRefreshingFlow.collectAsState()
var expanded by remember { mutableStateOf(false) }
Card(
onClick = {
if (!subsRefreshingFlow.value) {
val dragged by interactionSource.collectIsDraggedAsState()
val onClick = {
if (!dragged) {
if (isSelectedMode) {
onSelectedChange?.invoke()
} else if (!subsRefreshingFlow.value) {
expanded = true
}
},
}
}
Card(
onClick = onClick,
modifier = modifier.padding(16.dp, 2.dp),
shape = MaterialTheme.shapes.small,
interactionSource = interactionSource,
colors = CardDefaults.cardColors(
containerColor = if (isSelected) {
MaterialTheme.colorScheme.primaryContainer
} else {
Color.Unspecified
}
),
) {
SubsMenuItem(
expanded = expanded,
Expand Down Expand Up @@ -165,7 +186,8 @@ fun SubsItemCard(
Spacer(modifier = Modifier.width(10.dp))
Switch(
checked = subsItem.enable,
onCheckedChange = onCheckedChange,
enabled = !isSelectedMode,
onCheckedChange = if (isSelectedMode) null else onCheckedChange,
)
}
}
Expand Down Expand Up @@ -222,28 +244,12 @@ private fun SubsMenuItem(
}
DropdownMenuItem(
text = {
Text(text = "导出数据")
Text(text = "分享数据")
},
onClick = {
onExpandedChange(false)
vm.viewModelScope.launchTry(Dispatchers.IO) {
val transferDataFile = exportZipDir.resolve("${TransferData.TYPE}.json")
transferDataFile.writeText(
json.encodeToString(
exportTransferData(
listOf(
subItem.id
)
)
)
)
val file = exportZipDir.resolve("backup-${subItem.id}.zip")
if (file.exists()) {
file.delete()
}
ZipUtils.zipFiles(listOf(transferDataFile), file)
transferDataFile.delete()
context.shareFile(file, "分享数据文件")
context.shareSubs(subItem.id)
}
}
)
Expand All @@ -270,7 +276,7 @@ private fun SubsMenuItem(
}
)
}
if (subItem.id != -2L) {
if (subItem.id != LOCAL_SUBS_ID) {
DropdownMenuItem(
text = {
Text(text = "删除订阅", color = MaterialTheme.colorScheme.error)
Expand All @@ -283,10 +289,21 @@ private fun SubsMenuItem(
"是否删除订阅 ${subscription?.name ?: subItem.id} ?",
)
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, "分享数据文件")
}
Loading

0 comments on commit f34b1ca

Please sign in to comment.