mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
feat: 规则分类
This commit is contained in:
parent
7ad5917f71
commit
606dafe4c1
288
app/schemas/li.songe.gkd.db.AppDb/3.json
Normal file
288
app/schemas/li.songe.gkd.db.AppDb/3.json
Normal file
|
@ -0,0 +1,288 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "b52c1f25e2052865818be5151b6ac6a0",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "subs_item",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ctime",
|
||||
"columnName": "ctime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mtime",
|
||||
"columnName": "mtime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "enable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enableUpdate",
|
||||
"columnName": "enable_update",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "order",
|
||||
"columnName": "order",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updateUrl",
|
||||
"columnName": "update_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "snapshot",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "app_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "activityId",
|
||||
"columnName": "activity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appName",
|
||||
"columnName": "app_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appVersionCode",
|
||||
"columnName": "app_version_code",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appVersionName",
|
||||
"columnName": "app_version_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "screenHeight",
|
||||
"columnName": "screen_height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "screenWidth",
|
||||
"columnName": "screen_width",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLandscape",
|
||||
"columnName": "is_landscape",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "githubAssetId",
|
||||
"columnName": "github_asset_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "subs_config",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "enable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subsItemId",
|
||||
"columnName": "subs_item_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "app_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "groupKey",
|
||||
"columnName": "group_key",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "click_log",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `group_key` INTEGER NOT NULL, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "app_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "activityId",
|
||||
"columnName": "activity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "subsId",
|
||||
"columnName": "subs_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "groupKey",
|
||||
"columnName": "group_key",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ruleIndex",
|
||||
"columnName": "rule_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ruleKey",
|
||||
"columnName": "rule_key",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "category_config",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "enable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "subsItemId",
|
||||
"columnName": "subs_item_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryKey",
|
||||
"columnName": "category_key",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b52c1f25e2052865818be5151b6ac6a0')"
|
||||
]
|
||||
}
|
||||
}
|
47
app/src/main/kotlin/li/songe/gkd/data/CategoryConfig.kt
Normal file
47
app/src/main/kotlin/li/songe/gkd/data/CategoryConfig.kt
Normal file
|
@ -0,0 +1,47 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Entity(
|
||||
tableName = "category_config",
|
||||
)
|
||||
data class CategoryConfig(
|
||||
@PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "enable") val enable: Boolean? = null,
|
||||
@ColumnInfo(name = "subs_item_id") val subsItemId: Long,
|
||||
@ColumnInfo(name = "category_key") val categoryKey: Int,
|
||||
) {
|
||||
@Dao
|
||||
interface CategoryConfigDao {
|
||||
|
||||
@Update
|
||||
suspend fun update(vararg objects: CategoryConfig): Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg objects: CategoryConfig): List<Long>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg objects: CategoryConfig): Int
|
||||
|
||||
@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=:subsItemId AND category_key=:categoryKey")
|
||||
suspend fun deleteByCategoryKey(subsItemId: Long, categoryKey: Int): Int
|
||||
|
||||
@Query("SELECT * FROM category_config")
|
||||
fun query(): Flow<List<CategoryConfig>>
|
||||
|
||||
@Query("SELECT * FROM category_config WHERE subs_item_id=:subsItemId")
|
||||
fun queryConfig(subsItemId: Long): Flow<List<CategoryConfig>>
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
|
@ -13,8 +12,6 @@ import androidx.room.Update
|
|||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.util.subsFolder
|
||||
import java.io.File
|
||||
|
@ -22,7 +19,6 @@ import java.io.File
|
|||
@Entity(
|
||||
tableName = "subs_item",
|
||||
)
|
||||
@Parcelize
|
||||
data class SubsItem(
|
||||
@PrimaryKey @ColumnInfo(name = "id") val id: Long,
|
||||
|
||||
|
@ -33,9 +29,8 @@ data class SubsItem(
|
|||
@ColumnInfo(name = "order") val order: Int,
|
||||
@ColumnInfo(name = "update_url") val updateUrl: String? = null,
|
||||
|
||||
) : Parcelable {
|
||||
) {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val subsFile by lazy {
|
||||
File(subsFolder.absolutePath.plus("/${id}.json"))
|
||||
}
|
||||
|
@ -47,6 +42,7 @@ data class SubsItem(
|
|||
DbSet.subsItemDao.delete(this)
|
||||
DbSet.subsConfigDao.delete(id)
|
||||
DbSet.clickLogDao.deleteBySubsId(id)
|
||||
DbSet.categoryConfigDao.deleteBySubsItemId(id)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -77,6 +73,9 @@ data class SubsItem(
|
|||
@Delete
|
||||
suspend fun delete(vararg users: SubsItem): Int
|
||||
|
||||
@Query("UPDATE subs_item SET mtime=:mtime WHERE id=:id")
|
||||
suspend fun updateMtime(id: Long, mtime: Long): Int
|
||||
|
||||
@Query("SELECT * FROM subs_item ORDER BY `order`")
|
||||
fun query(): Flow<List<SubsItem>>
|
||||
|
||||
|
|
|
@ -28,8 +28,34 @@ data class SubscriptionRaw(
|
|||
val supportUri: String? = null,
|
||||
val checkUpdateUrl: String? = null,
|
||||
val apps: List<AppRaw> = emptyList(),
|
||||
val categories: List<Category> = emptyList(),
|
||||
) {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val categoriesGroups by lazy {
|
||||
val allAppGroups =
|
||||
apps.flatMap { a -> a.groups.map { g -> g to a } }
|
||||
allAppGroups.groupBy { g ->
|
||||
categories.find { c -> g.first.name.startsWith(c.name) }
|
||||
}
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
val groupToCategoryMap by lazy {
|
||||
val map = mutableMapOf<GroupRaw, Category>()
|
||||
categoriesGroups.forEach { (key, value) ->
|
||||
value.forEach { (g) ->
|
||||
if (key != null) {
|
||||
map[g] = key
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Category(val key: Int, val name: String, val enable: Boolean?)
|
||||
|
||||
interface CommonProps {
|
||||
val activityIds: List<String>?
|
||||
val excludeActivityIds: List<String>?
|
||||
|
@ -306,7 +332,17 @@ data class SubscriptionRaw(
|
|||
jsonToAppRaw(
|
||||
jsonElement.jsonObject, index
|
||||
)
|
||||
} ?: emptyList())
|
||||
} ?: emptyList(),
|
||||
categories = rootJson["categories"]?.jsonArray?.mapIndexed { index, jsonElement ->
|
||||
Category(
|
||||
key = getInt(jsonElement.jsonObject, "key")
|
||||
?: error("miss categories[$index].key"),
|
||||
name = getString(jsonElement.jsonObject, "name")
|
||||
?: error("miss categories[$index].name"),
|
||||
enable = getBoolean(jsonElement.jsonObject, "enable"),
|
||||
)
|
||||
} ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
// 订阅文件状态: 文件不存在, 文件正常, 文件损坏(损坏原因)
|
||||
|
@ -314,19 +350,26 @@ data class SubscriptionRaw(
|
|||
|
||||
fun parse(source: String, json5: Boolean = true): SubscriptionRaw {
|
||||
val text = if (json5) Jankson.builder().build().load(source).toJson() else source
|
||||
|
||||
val obj = jsonToSubscriptionRaw(json.parseToJsonElement(text).jsonObject)
|
||||
|
||||
// 校验 category 不重复
|
||||
obj.categories.forEach { c ->
|
||||
if (obj.categories.find { c2 -> c2 !== c && c2.key == c.key } != null) {
|
||||
error("duplicated category: key:${c.key} ")
|
||||
}
|
||||
}
|
||||
// 校验 appId 不重复
|
||||
val duplicatedApps = obj.apps.groupingBy { it }.eachCount().filter { it.value > 1 }.keys
|
||||
if (duplicatedApps.isNotEmpty()) {
|
||||
error("duplicated app: ${duplicatedApps.map { it.id }}")
|
||||
}
|
||||
obj.apps.forEach { appRaw ->
|
||||
// 校验 group key 不重复
|
||||
val duplicatedGroups =
|
||||
appRaw.groups.groupingBy { it }.eachCount().filter { it.value > 1 }.keys
|
||||
if (duplicatedGroups.isNotEmpty()) {
|
||||
error("app:${appRaw.id}, duplicated group: ${duplicatedGroups.map { it.key }}")
|
||||
}
|
||||
// 校验 rule key 不重复
|
||||
appRaw.groups.forEach { groupRaw ->
|
||||
val duplicatedRules =
|
||||
groupRaw.rules.mapNotNull { r -> r.key }.groupingBy { it }.eachCount()
|
||||
|
@ -336,7 +379,6 @@ data class SubscriptionRaw(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
|
|
|
@ -3,19 +3,21 @@ package li.songe.gkd.db
|
|||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import li.songe.gkd.data.CategoryConfig
|
||||
import li.songe.gkd.data.ClickLog
|
||||
import li.songe.gkd.data.Snapshot
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.SubsItem
|
||||
|
||||
@Database(
|
||||
version = 2,
|
||||
entities = [SubsItem::class, Snapshot::class, SubsConfig::class, ClickLog::class],
|
||||
autoMigrations = [AutoMigration(from = 1, to = 2)]
|
||||
version = 3,
|
||||
entities = [SubsItem::class, Snapshot::class, SubsConfig::class, ClickLog::class, CategoryConfig::class],
|
||||
autoMigrations = [AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3)]
|
||||
)
|
||||
abstract class AppDb : RoomDatabase() {
|
||||
abstract fun subsItemDao(): SubsItem.SubsItemDao
|
||||
abstract fun snapshotDao(): Snapshot.SnapshotDao
|
||||
abstract fun subsConfigDao(): SubsConfig.SubsConfigDao
|
||||
abstract fun clickLogDao(): ClickLog.TriggerLogDao
|
||||
abstract fun categoryConfigDao(): CategoryConfig.CategoryConfigDao
|
||||
}
|
|
@ -30,6 +30,7 @@ object DbSet {
|
|||
val subsConfigDao by lazy { appDb.subsConfigDao() }
|
||||
val snapshotDao by lazy { appDb.snapshotDao() }
|
||||
val clickLogDao by lazy { appDb.clickLogDao() }
|
||||
val categoryConfigDao by lazy { appDb.categoryConfigDao() }
|
||||
|
||||
private fun createCallback(): RoomDatabase.Callback {
|
||||
return object : RoomDatabase.Callback() {
|
||||
|
|
|
@ -63,12 +63,11 @@ import li.songe.gkd.util.LocalNavController
|
|||
import li.songe.gkd.util.ProfileTransitions
|
||||
import li.songe.gkd.util.appInfoCacheFlow
|
||||
import li.songe.gkd.util.encodeToJson5String
|
||||
import li.songe.gkd.util.getGroupRawEnable
|
||||
import li.songe.gkd.util.json
|
||||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.navigate
|
||||
import li.songe.gkd.util.storeFlow
|
||||
import li.songe.gkd.util.subsIdToRawFlow
|
||||
|
||||
@RootNavGraph
|
||||
@Destination(style = ProfileTransitions::class)
|
||||
|
@ -82,13 +81,15 @@ fun AppItemPage(
|
|||
val navController = LocalNavController.current
|
||||
val vm = hiltViewModel<AppItemVm>()
|
||||
val subsItem by vm.subsItemFlow.collectAsState()
|
||||
val subsRaw = vm.subsRawFlow.collectAsState().value
|
||||
val subsConfigs by vm.subsConfigsFlow.collectAsState()
|
||||
val categoryConfigs by vm.categoryConfigsFlow.collectAsState()
|
||||
val appRaw by vm.subsAppFlow.collectAsState()
|
||||
val appInfoCache by appInfoCacheFlow.collectAsState()
|
||||
val store by storeFlow.collectAsState()
|
||||
|
||||
val appRawVal = appRaw
|
||||
val subsItemVal = subsItem
|
||||
val groupToCategoryMap = subsRaw?.groupToCategoryMap ?: emptyMap()
|
||||
|
||||
val (showGroupItem, setShowGroupItem) = remember {
|
||||
mutableStateOf<SubscriptionRaw.GroupRaw?>(
|
||||
|
@ -118,10 +119,12 @@ fun AppItemPage(
|
|||
)
|
||||
}
|
||||
}, title = {
|
||||
Text(
|
||||
text = if (subsItem == null) "订阅文件缺失" else (appInfoCache[appRaw?.id]?.name
|
||||
?: appRaw?.name ?: appRaw?.id ?: "")
|
||||
)
|
||||
val text = if (subsRaw == null) {
|
||||
"订阅文件缺失"
|
||||
} else {
|
||||
"${subsRaw.name}/${appInfoCache[appRaw?.id]?.name ?: appRaw?.name ?: appRaw?.id}"
|
||||
}
|
||||
Text(text = text)
|
||||
}, actions = {})
|
||||
}, floatingActionButton = {
|
||||
if (editable) {
|
||||
|
@ -200,10 +203,14 @@ fun AppItemPage(
|
|||
Spacer(modifier = Modifier.width(10.dp))
|
||||
}
|
||||
|
||||
val groupEnable = getGroupRawEnable(
|
||||
group,
|
||||
subsConfigs,
|
||||
groupToCategoryMap[group],
|
||||
categoryConfigs
|
||||
)
|
||||
val subsConfig = subsConfigs.find { it.groupKey == group.key }
|
||||
Switch(checked = (subsConfig?.enable ?: store.enableGroup ?: group.enable
|
||||
?: true),
|
||||
modifier = Modifier,
|
||||
Switch(checked = groupEnable, modifier = Modifier,
|
||||
onCheckedChange = scope.launchAsFn { enable ->
|
||||
val newItem = (subsConfig?.copy(enable = enable) ?: SubsConfig(
|
||||
type = SubsConfig.GroupType,
|
||||
|
@ -290,7 +297,7 @@ fun AppItemPage(
|
|||
Text(text = "删除", modifier = Modifier
|
||||
.clickable {
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
val subsRaw = subsIdToRawFlow.value[subsItemId] ?: return@launchTry
|
||||
subsRaw ?: return@launchTry
|
||||
val newSubsRaw = subsRaw.copy(
|
||||
apps = subsRaw.apps
|
||||
.toMutableList()
|
||||
|
@ -364,7 +371,7 @@ fun AppItemPage(
|
|||
return@TextButton
|
||||
}
|
||||
setEditGroupRaw(null)
|
||||
val subsRaw = subsIdToRawFlow.value[subsItemId] ?: return@TextButton
|
||||
subsRaw ?: return@TextButton
|
||||
val newSubsRaw = subsRaw.copy(apps = subsRaw.apps.toMutableList().apply {
|
||||
set(
|
||||
indexOfFirst { a -> a.id == appRawVal.id },
|
||||
|
@ -441,7 +448,7 @@ fun AppItemPage(
|
|||
}
|
||||
}
|
||||
val newKey = appRawVal.groups.maxBy { g -> g.key }.key + 1
|
||||
val subsRaw = subsIdToRawFlow.value[subsItemId] ?: return@TextButton
|
||||
subsRaw ?: return@TextButton
|
||||
val newSubsRaw = subsRaw.copy(apps = subsRaw.apps.toMutableList().apply {
|
||||
set(
|
||||
indexOfFirst { a -> a.id == appRawVal.id },
|
||||
|
|
|
@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.flow.stateIn
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.destinations.AppItemPageDestination
|
||||
import li.songe.gkd.util.map
|
||||
import li.songe.gkd.util.subsIdToRawFlow
|
||||
import li.songe.gkd.util.subsItemsFlow
|
||||
import javax.inject.Inject
|
||||
|
@ -17,14 +18,18 @@ import javax.inject.Inject
|
|||
class AppItemVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel() {
|
||||
private val args = AppItemPageDestination.argsFrom(stateHandle)
|
||||
|
||||
|
||||
val subsItemFlow =
|
||||
subsItemsFlow.map { subsItems -> subsItems.find { s -> s.id == args.subsItemId } }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
val subsRawFlow = subsIdToRawFlow.map(viewModelScope) { s -> s[args.subsItemId] }
|
||||
|
||||
val subsConfigsFlow = DbSet.subsConfigDao.queryGroupTypeConfig(args.subsItemId, args.appId)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val categoryConfigsFlow = DbSet.categoryConfigDao.queryConfig(args.subsItemId)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val subsAppFlow =
|
||||
subsIdToRawFlow.map { subsIdToRaw -> subsIdToRaw[args.subsItemId]?.apps?.find { it.id == args.appId } }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
|
365
app/src/main/kotlin/li/songe/gkd/ui/CategoryPage.kt
Normal file
365
app/src/main/kotlin/li/songe/gkd/ui/CategoryPage.kt
Normal file
|
@ -0,0 +1,365 @@
|
|||
package li.songe.gkd.ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
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.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.data.CategoryConfig
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.ProfileTransitions
|
||||
import li.songe.gkd.util.json
|
||||
import li.songe.gkd.util.launchTry
|
||||
|
||||
@RootNavGraph
|
||||
@Destination(style = ProfileTransitions::class)
|
||||
@Composable
|
||||
fun CategoryPage(subsItemId: Long) {
|
||||
val navController = LocalNavController.current
|
||||
val vm = hiltViewModel<CategoryVm>()
|
||||
val subsItem by vm.subsItemFlow.collectAsState()
|
||||
val subsRaw by vm.subsRawFlow.collectAsState()
|
||||
val categoryConfigs by vm.categoryConfigsFlow.collectAsState()
|
||||
val editable = subsItem != null && subsItemId < 0
|
||||
|
||||
var showAddDlg by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val (menuCategory, setMenuCategory) = remember {
|
||||
mutableStateOf<SubscriptionRaw.Category?>(null)
|
||||
}
|
||||
var editEnableCategory by remember {
|
||||
mutableStateOf<SubscriptionRaw.Category?>(null)
|
||||
}
|
||||
val (editNameCategory, setEditNameCategory) = remember {
|
||||
mutableStateOf<SubscriptionRaw.Category?>(null)
|
||||
}
|
||||
|
||||
val categories = subsRaw?.categories ?: emptyList()
|
||||
val categoriesGroups = subsRaw?.categoriesGroups ?: emptyMap()
|
||||
|
||||
Scaffold(topBar = {
|
||||
TopAppBar(navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}, title = { Text(text = subsRaw?.name ?: subsItemId.toString()) }, actions = {})
|
||||
}, floatingActionButton = {
|
||||
if (editable) {
|
||||
FloatingActionButton(onClick = { showAddDlg = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = "add",
|
||||
)
|
||||
}
|
||||
}
|
||||
}, content = { contentPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(contentPadding)
|
||||
) {
|
||||
items(categories, { it.key }) { category ->
|
||||
Row(modifier = Modifier
|
||||
.clickable {
|
||||
editEnableCategory = category
|
||||
}
|
||||
.padding(10.dp, 6.dp), verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val size = categoriesGroups[category]?.size ?: 0
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = category.name, fontSize = 18.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = if (size > 0) "${size}规则组" else "暂无规则", fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
if (editable) {
|
||||
IconButton(onClick = {
|
||||
setMenuCategory(category)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val categoryConfig =
|
||||
categoryConfigs.find { c -> c.categoryKey == category.key }
|
||||
val enable =
|
||||
if (categoryConfig != null) categoryConfig.enable else category.enable
|
||||
Text(
|
||||
text = enableGroupRadioOptions.find { e -> e.second == enable }?.first
|
||||
?: "",
|
||||
fontSize = 14.sp
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowRight,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
if (categories.isEmpty()) {
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
Text(
|
||||
text = "此订阅暂无类别",
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
editEnableCategory?.let { category ->
|
||||
val categoryConfig =
|
||||
categoryConfigs.find { c -> c.categoryKey == category.key }
|
||||
val enable =
|
||||
if (categoryConfig != null) categoryConfig.enable else category.enable
|
||||
Dialog(onDismissRequest = { editEnableCategory = null }) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Column {
|
||||
enableGroupRadioOptions.forEach { option ->
|
||||
val onClick: () -> Unit = {
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
DbSet.categoryConfigDao.insert(
|
||||
(categoryConfig ?: CategoryConfig(
|
||||
enable = option.second,
|
||||
subsItemId = subsItemId,
|
||||
categoryKey = category.key
|
||||
)).copy(enable = option.second)
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = (option.second == enable),
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = (option.second == enable),
|
||||
onClick = onClick
|
||||
)
|
||||
Text(
|
||||
text = option.first, modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val subsRawVal = subsRaw
|
||||
if (editNameCategory != null && subsRawVal != null) {
|
||||
var source by remember {
|
||||
mutableStateOf(editNameCategory.name)
|
||||
}
|
||||
AlertDialog(
|
||||
title = { Text(text = "编辑类别") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = source,
|
||||
onValueChange = { source = it.trim() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text(text = "请输入类别名称") },
|
||||
singleLine = true
|
||||
)
|
||||
},
|
||||
onDismissRequest = { setEditNameCategory(null) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = { setEditNameCategory(null) }) {
|
||||
Text(text = "取消")
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = source.isNotEmpty() && source != editNameCategory.name,
|
||||
onClick = {
|
||||
if (categories.any { c -> c.key != editNameCategory.key && c.name == source }) {
|
||||
ToastUtils.showShort("不可添加同名类别")
|
||||
return@TextButton
|
||||
}
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
subsItem?.apply {
|
||||
subsFile.writeText(json.encodeToString(subsRawVal.copy(
|
||||
categories = categories.toMutableList().apply {
|
||||
val i =
|
||||
categories.indexOfFirst { c -> c.key == editNameCategory.key }
|
||||
if (i >= 0) {
|
||||
set(i, editNameCategory.copy(name = source))
|
||||
}
|
||||
}
|
||||
)))
|
||||
DbSet.subsItemDao.update(copy(mtime = System.currentTimeMillis()))
|
||||
}
|
||||
ToastUtils.showShort("修改成功")
|
||||
setEditNameCategory(null)
|
||||
}
|
||||
}) {
|
||||
Text(text = "确认")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
if (showAddDlg && subsRawVal != null) {
|
||||
var source by remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
AlertDialog(
|
||||
title = { Text(text = "添加类别") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = source,
|
||||
onValueChange = { source = it.trim() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text(text = "请输入类别名称") },
|
||||
singleLine = true
|
||||
)
|
||||
},
|
||||
onDismissRequest = { showAddDlg = false },
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showAddDlg = false }) {
|
||||
Text(text = "取消")
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(enabled = source.isNotEmpty(), onClick = {
|
||||
if (categories.any { c -> c.name == source }) {
|
||||
ToastUtils.showShort("不可添加同名类别")
|
||||
return@TextButton
|
||||
}
|
||||
showAddDlg = false
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
subsItem?.apply {
|
||||
subsFile.writeText(json.encodeToString(subsRawVal.copy(
|
||||
categories = categories.toMutableList().apply {
|
||||
add(SubscriptionRaw.Category(
|
||||
key = (categories.maxOfOrNull { c -> c.key } ?: -1) + 1,
|
||||
name = source,
|
||||
enable = null
|
||||
))
|
||||
}
|
||||
)))
|
||||
DbSet.subsItemDao.update(copy(mtime = System.currentTimeMillis()))
|
||||
ToastUtils.showShort("添加成功")
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(text = "确认")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (menuCategory != null && subsRawVal != null) {
|
||||
Dialog(onDismissRequest = { setMenuCategory(null) }) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Column {
|
||||
Text(text = "编辑", modifier = Modifier
|
||||
.clickable {
|
||||
setEditNameCategory(menuCategory)
|
||||
setMenuCategory(null)
|
||||
}
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth())
|
||||
Text(text = "删除", modifier = Modifier
|
||||
.clickable {
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
subsItem?.apply {
|
||||
subsFile.writeText(json.encodeToString(subsRawVal.copy(
|
||||
categories = subsRawVal.categories.filter { c -> c.key != menuCategory.key }
|
||||
)))
|
||||
DbSet.subsItemDao.update(copy(mtime = System.currentTimeMillis()))
|
||||
}
|
||||
DbSet.categoryConfigDao.deleteByCategoryKey(
|
||||
subsItemId,
|
||||
menuCategory.key
|
||||
)
|
||||
ToastUtils.showShort("删除成功")
|
||||
setMenuCategory(null)
|
||||
}
|
||||
}
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
27
app/src/main/kotlin/li/songe/gkd/ui/CategoryVm.kt
Normal file
27
app/src/main/kotlin/li/songe/gkd/ui/CategoryVm.kt
Normal file
|
@ -0,0 +1,27 @@
|
|||
package li.songe.gkd.ui
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.destinations.CategoryPageDestination
|
||||
import li.songe.gkd.util.map
|
||||
import li.songe.gkd.util.subsIdToRawFlow
|
||||
import li.songe.gkd.util.subsItemsFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class CategoryVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel() {
|
||||
private val args = CategoryPageDestination.argsFrom(stateHandle)
|
||||
|
||||
val subsItemFlow =
|
||||
subsItemsFlow.map(viewModelScope) { subsItems -> subsItems.find { s -> s.id == args.subsItemId } }
|
||||
|
||||
val subsRawFlow = subsIdToRawFlow.map(viewModelScope) { m -> m[args.subsItemId] }
|
||||
|
||||
val categoryConfigsFlow = DbSet.categoryConfigDao.queryConfig(args.subsItemId)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
}
|
|
@ -82,9 +82,6 @@ fun SettingsPage() {
|
|||
var showEnableDarkThemeDlg by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var showEnableGroupDlg by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var showToastInputDlg by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
@ -209,27 +206,6 @@ fun SettingsPage() {
|
|||
}
|
||||
}
|
||||
Divider()
|
||||
Row(modifier = Modifier
|
||||
.clickable {
|
||||
showEnableGroupDlg = true
|
||||
}
|
||||
.padding(10.dp, 15.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f), text = "规则启用", fontSize = 18.sp
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = enableGroupRadioOptions.find { it.second == store.enableGroup }?.first
|
||||
?: store.enableGroup.toString(), fontSize = 14.sp
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowRight, contentDescription = "more"
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
|
||||
TextSwitch(name = "保存日志",
|
||||
desc = "保存最近7天的日志,大概占用您5M的空间",
|
||||
|
@ -360,43 +336,6 @@ fun SettingsPage() {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (showEnableGroupDlg) {
|
||||
Dialog(onDismissRequest = { showEnableGroupDlg = false }) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Column {
|
||||
enableGroupRadioOptions.forEach { option ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(selected = (option.second == store.enableGroup),
|
||||
onClick = {
|
||||
updateStorage(
|
||||
storeFlow,
|
||||
storeFlow.value.copy(enableGroup = option.second)
|
||||
)
|
||||
})
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
RadioButton(selected = (option.second == store.enableGroup), onClick = {
|
||||
updateStorage(
|
||||
storeFlow, storeFlow.value.copy(enableGroup = option.second)
|
||||
)
|
||||
})
|
||||
Text(
|
||||
text = option.first, modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showToastInputDlg) {
|
||||
var value by remember {
|
||||
|
@ -570,8 +509,8 @@ private val darkThemeRadioOptions = listOf(
|
|||
"启用" to true,
|
||||
"关闭" to false,
|
||||
)
|
||||
private val enableGroupRadioOptions = listOf(
|
||||
val enableGroupRadioOptions = listOf(
|
||||
"跟随订阅" to null,
|
||||
"默认启用" to true,
|
||||
"默认关闭" to false,
|
||||
"全部启用" to true,
|
||||
"全部关闭" to false,
|
||||
)
|
|
@ -57,6 +57,7 @@ import li.songe.gkd.data.SubsItem
|
|||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.component.SubsItemCard
|
||||
import li.songe.gkd.ui.destinations.CategoryPageDestination
|
||||
import li.songe.gkd.ui.destinations.SubsPageDestination
|
||||
import li.songe.gkd.util.DEFAULT_SUBS_UPDATE_URL
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
|
@ -217,6 +218,14 @@ fun SubsManagePage() {
|
|||
.fillMaxWidth()
|
||||
.padding(16.dp))
|
||||
Divider()
|
||||
Text(text = "查看类别", modifier = Modifier
|
||||
.clickable {
|
||||
menuSubItem = null
|
||||
navController.navigate(CategoryPageDestination(subsRawVal.id))
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp))
|
||||
Divider()
|
||||
}
|
||||
if (menuSubItemVal.id < 0 && subsRawVal != null && menuSubItemVal.subsFile.exists()) {
|
||||
Text(text = "分享文件", modifier = Modifier
|
||||
|
|
|
@ -66,7 +66,6 @@ import li.songe.gkd.util.json
|
|||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.navigate
|
||||
import li.songe.gkd.util.subsIdToRawFlow
|
||||
|
||||
|
||||
@RootNavGraph
|
||||
|
@ -80,12 +79,11 @@ fun SubsPage(
|
|||
|
||||
val vm = hiltViewModel<SubsVm>()
|
||||
val subsItem by vm.subsItemFlow.collectAsState()
|
||||
val subsIdToRaw by subsIdToRawFlow.collectAsState()
|
||||
val appAndConfigs by vm.filterAppAndConfigsFlow.collectAsState()
|
||||
val searchStr by vm.searchStrFlow.collectAsState()
|
||||
val appInfoCache by appInfoCacheFlow.collectAsState()
|
||||
|
||||
val subsRaw = subsIdToRaw[subsItem?.id]
|
||||
val subsRaw = vm.subsRawFlow.collectAsState().value
|
||||
|
||||
// 本地订阅
|
||||
val editable = subsItem?.id.let { it != null && it < 0 }
|
||||
|
|
|
@ -15,8 +15,8 @@ import li.songe.gkd.data.Tuple3
|
|||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.destinations.SubsPageDestination
|
||||
import li.songe.gkd.util.appInfoCacheFlow
|
||||
import li.songe.gkd.util.getGroupRawEnable
|
||||
import li.songe.gkd.util.map
|
||||
import li.songe.gkd.util.storeFlow
|
||||
import li.songe.gkd.util.subsIdToRawFlow
|
||||
import li.songe.gkd.util.subsItemsFlow
|
||||
import java.text.Collator
|
||||
|
@ -30,17 +30,22 @@ class SubsVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel() {
|
|||
val subsItemFlow =
|
||||
subsItemsFlow.map(viewModelScope) { s -> s.find { v -> v.id == args.subsItemId } }
|
||||
|
||||
val subsRawFlow = subsIdToRawFlow.map(viewModelScope) { s -> s[args.subsItemId] }
|
||||
|
||||
private val appSubsConfigsFlow = DbSet.subsConfigDao.queryAppTypeConfig(args.subsItemId)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private val groupSubsConfigsFlow = DbSet.subsConfigDao.querySubsGroupTypeConfig(args.subsItemId)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private val appsFlow = combine(
|
||||
subsItemFlow, subsIdToRawFlow, appInfoCacheFlow
|
||||
) { subsItem, subsIdToRaw, appInfoCache ->
|
||||
private val categoryConfigsFlow = DbSet.categoryConfigDao.queryConfig(args.subsItemId)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private val sortAppsFlow = combine(
|
||||
subsRawFlow, appInfoCacheFlow
|
||||
) { subsRaw, appInfoCache ->
|
||||
val collator = Collator.getInstance(Locale.CHINESE)
|
||||
(subsIdToRaw[subsItem?.id]?.apps ?: emptyList()).sortedWith { a, b ->
|
||||
(subsRaw?.apps ?: emptyList()).sortedWith { a, b ->
|
||||
// 顺序: 已安装(有名字->无名字)->未安装(有名字(来自订阅)->无名字)
|
||||
collator.compare(appInfoCache[a.id]?.name ?: a.name?.let { "\uFFFF" + it }
|
||||
?: ("\uFFFF\uFFFF" + a.id),
|
||||
|
@ -54,18 +59,20 @@ class SubsVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel() {
|
|||
private val debounceSearchStr = searchStrFlow.debounce(200)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, searchStrFlow.value)
|
||||
|
||||
private val appAndConfigsFlow = combine(appsFlow,
|
||||
private val appAndConfigsFlow = combine(
|
||||
subsRawFlow,
|
||||
sortAppsFlow,
|
||||
categoryConfigsFlow,
|
||||
appSubsConfigsFlow,
|
||||
groupSubsConfigsFlow,
|
||||
storeFlow.map(viewModelScope) { s -> s.enableGroup }) { apps, appSubsConfigs, groupSubsConfigs, enableGroup ->
|
||||
) { subsRaw, apps, categoryConfigs, appSubsConfigs, groupSubsConfigs ->
|
||||
val groupToCategoryMap = subsRaw?.groupToCategoryMap ?: emptyMap()
|
||||
apps.map { app ->
|
||||
val subsConfig = appSubsConfigs.find { s -> s.appId == app.id }
|
||||
val appGroupSubsConfigs = groupSubsConfigs.filter { s -> s.appId == app.id }
|
||||
val enableSize = app.groups.count { g ->
|
||||
appGroupSubsConfigs.find { s -> s.groupKey == g.key }?.enable ?: enableGroup
|
||||
?: g.enable ?: true
|
||||
getGroupRawEnable(g, appGroupSubsConfigs, groupToCategoryMap[g], categoryConfigs)
|
||||
}
|
||||
Tuple3(app, subsConfig, enableSize)
|
||||
Tuple3(app, appSubsConfigs.find { s -> s.appId == app.id }, enableSize)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package li.songe.gkd.ui.component
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
data class DialogParams(
|
||||
val title: String,
|
||||
val text: String? = null,
|
||||
val resolve: () -> Unit,
|
||||
val reject: () -> Unit
|
||||
)
|
||||
|
||||
val dialogParamsFlow = MutableStateFlow<DialogParams?>(null)
|
||||
|
||||
@Composable
|
||||
fun ConfirmDialog() {
|
||||
val dialogParams = dialogParamsFlow.collectAsState().value
|
||||
if (dialogParams != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { },
|
||||
title = { Text(text = dialogParams.title) },
|
||||
text = if (dialogParams.text != null) {
|
||||
{
|
||||
Text(text = dialogParams.text)
|
||||
}
|
||||
} else null,
|
||||
confirmButton = {
|
||||
TextButton(onClick = dialogParams.resolve) {
|
||||
Text(text = "是", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = dialogParams.reject) {
|
||||
Text(text = "否")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
DisposableEffect(key1 = null, effect = {
|
||||
onDispose {
|
||||
val d = dialogParamsFlow.value
|
||||
if (d != null) {
|
||||
d.reject.invoke()
|
||||
dialogParamsFlow.value = null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
suspend fun getDialogResult(title: String, text: String? = null): Boolean {
|
||||
return suspendCoroutine { s ->
|
||||
dialogParamsFlow.value = DialogParams(
|
||||
title = title,
|
||||
text = text,
|
||||
resolve = { s.resume(true);dialogParamsFlow.value = null },
|
||||
reject = { s.resume(false);dialogParamsFlow.value = null }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -59,7 +59,6 @@ data class Store(
|
|||
val log2FileSwitch: Boolean = true,
|
||||
val enableDarkTheme: Boolean? = null,
|
||||
val enableAbFloatWindow: Boolean = true,
|
||||
val enableGroup: Boolean? = null,
|
||||
val matchUnknownApp: Boolean = false,
|
||||
)
|
||||
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import li.songe.gkd.appScope
|
||||
import li.songe.gkd.data.CategoryConfig
|
||||
import li.songe.gkd.data.Rule
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.selector.Selector
|
||||
|
||||
|
@ -17,48 +21,67 @@ val subsItemsFlow by lazy {
|
|||
}
|
||||
|
||||
private val subsIdToMtimeFlow by lazy {
|
||||
DbSet.subsItemDao.query().map { it.sortedBy { s -> s.id }.associate { s -> s.id to s.mtime } }
|
||||
subsItemsFlow.map { it.sortedBy { s -> s.id }.associate { s -> s.id to s.mtime } }
|
||||
.stateIn(appScope, SharingStarted.Eagerly, emptyMap())
|
||||
}
|
||||
|
||||
val subsIdToRawFlow by lazy {
|
||||
subsIdToMtimeFlow.map { subsIdToMtime ->
|
||||
subsIdToMtime.map { entry ->
|
||||
entry.key to SubsItem.getSubscriptionRaw(entry.key)
|
||||
}.toMap()
|
||||
}.onEach { rawMap ->
|
||||
updateAppInfo(*rawMap.values.map { subsRaw ->
|
||||
subsRaw?.apps?.map { a -> a.id }
|
||||
}.flatMap { it ?: emptyList() }.toTypedArray())
|
||||
}.stateIn(appScope, SharingStarted.Eagerly, emptyMap())
|
||||
MutableStateFlow<Map<Long, SubscriptionRaw?>>(emptyMap())
|
||||
}
|
||||
|
||||
val subsConfigsFlow by lazy {
|
||||
DbSet.subsConfigDao.query().stateIn(appScope, SharingStarted.Eagerly, emptyList())
|
||||
fun getGroupRawEnable(
|
||||
groupRaw: SubscriptionRaw.GroupRaw,
|
||||
subsConfigs: List<SubsConfig>,
|
||||
category: SubscriptionRaw.Category?,
|
||||
categoryConfigs: List<CategoryConfig>
|
||||
): Boolean {
|
||||
// 优先级: 规则用户配置 > 批量配置 > 批量默认 > 规则默认
|
||||
val groupConfig = subsConfigs.find { c -> c.groupKey == groupRaw.key }
|
||||
// 1.规则用户配置
|
||||
return groupConfig?.enable ?: if (category != null) {// 这个规则被批量配置捕获
|
||||
val categoryConfig = categoryConfigs.find { c -> c.categoryKey == category.key }
|
||||
val enable = if (categoryConfig != null) {
|
||||
// 2.批量配置
|
||||
categoryConfig.enable
|
||||
} else {
|
||||
// 3.批量默认
|
||||
category.enable
|
||||
}
|
||||
enable
|
||||
} else {
|
||||
null
|
||||
} ?: groupRaw.enable ?: true
|
||||
}
|
||||
|
||||
private val appIdToRulesFlow by lazy {
|
||||
combine(subsItemsFlow,
|
||||
combine(
|
||||
subsItemsFlow,
|
||||
subsIdToRawFlow,
|
||||
subsConfigsFlow,
|
||||
storeFlow.map(appScope) { s -> s.enableGroup }) { subsItems, subsIdToRaw, subsConfigs, enableGroup ->
|
||||
val appSubsConfigs = subsConfigs.filter { it.type == SubsConfig.AppType }
|
||||
val groupSubsConfigs = subsConfigs.filter { it.type == SubsConfig.GroupType }
|
||||
DbSet.subsConfigDao.query(),
|
||||
DbSet.categoryConfigDao.query(),
|
||||
) { subsItems, subsIdToRaw, subsConfigs, categoryConfigs ->
|
||||
val appSubsConfigs = subsConfigs.filter { c -> c.type == SubsConfig.AppType }
|
||||
val groupSubsConfigs = subsConfigs.filter { c -> c.type == SubsConfig.GroupType }
|
||||
val appIdToRules = mutableMapOf<String, MutableList<Rule>>()
|
||||
subsItems.filter { it.enable }.forEach { subsItem ->
|
||||
(subsIdToRaw[subsItem.id]?.apps ?: emptyList()).filter { appRaw ->
|
||||
val subsRaw = subsIdToRaw[subsItem.id] ?: return@forEach
|
||||
val subAppSubsConfigs = appSubsConfigs.filter { c -> c.subsItemId == subsItem.id }
|
||||
val subGroupSubsConfigs = groupSubsConfigs.filter { c -> c.subsItemId == subsItem.id }
|
||||
val subCategoryConfigs = categoryConfigs.filter { c -> c.subsItemId == subsItem.id }
|
||||
subsRaw.apps.filter { appRaw ->
|
||||
// 筛选 当前启用的 app 订阅规则
|
||||
(appSubsConfigs.find { subsConfig ->
|
||||
subsConfig.subsItemId == subsItem.id && subsConfig.appId == appRaw.id
|
||||
}?.enable ?: true)
|
||||
(subAppSubsConfigs.find { c -> c.appId == appRaw.id }?.enable ?: true)
|
||||
}.forEach { appRaw ->
|
||||
val appGroupConfigs = subGroupSubsConfigs.filter { c -> c.appId == appRaw.id }
|
||||
val rules = appIdToRules[appRaw.id] ?: mutableListOf()
|
||||
appIdToRules[appRaw.id] = rules
|
||||
appRaw.groups.filter { groupRaw ->
|
||||
// 筛选已经启用的规则组
|
||||
groupSubsConfigs.find { subsConfig ->
|
||||
subsConfig.subsItemId == subsItem.id && subsConfig.appId == appRaw.id && subsConfig.groupKey == groupRaw.key
|
||||
}?.enable ?: enableGroup ?: groupRaw.enable ?: true
|
||||
getGroupRawEnable(
|
||||
groupRaw,
|
||||
appGroupConfigs,
|
||||
subsRaw.groupToCategoryMap[groupRaw],
|
||||
subCategoryConfigs
|
||||
)
|
||||
}.filter { groupRaw ->
|
||||
// 筛选合法选择器的规则组, 如果一个规则组内某个选择器语法错误, 则禁用/丢弃此规则组
|
||||
groupRaw.valid
|
||||
|
@ -155,7 +178,7 @@ private val appIdToRulesFlow by lazy {
|
|||
rules.sortBy { r -> if (r.isOpenAd) 0 else 1 }
|
||||
}
|
||||
appIdToRules.filter { it.value.isNotEmpty() }
|
||||
}.stateIn<Map<String, List<Rule>>>(appScope, SharingStarted.Eagerly, emptyMap())
|
||||
}.stateIn(appScope, SharingStarted.Eagerly, emptyMap<String, List<Rule>>())
|
||||
}
|
||||
|
||||
data class AppRule(
|
||||
|
@ -184,7 +207,26 @@ val appRuleFlow by lazy {
|
|||
|
||||
fun initSubsState() {
|
||||
subsItemsFlow.value
|
||||
subsIdToMtimeFlow.value
|
||||
subsIdToRawFlow.value
|
||||
subsConfigsFlow.value
|
||||
appScope.launchTry {
|
||||
// reload subsRaw file by diff
|
||||
var oldSubsIdToMtime = emptyMap<Long, Long>()
|
||||
subsIdToMtimeFlow.collect { subsIdToMtime ->
|
||||
val commonMap = subsIdToMtime.filter { e -> oldSubsIdToMtime[e.key] == e.value }
|
||||
oldSubsIdToMtime = subsIdToMtime
|
||||
val oldSubsIdToRaw = subsIdToRawFlow.value
|
||||
subsIdToRawFlow.value = subsIdToMtime.map { entry ->
|
||||
if (commonMap.containsKey(entry.key)) {
|
||||
entry.key to oldSubsIdToRaw[entry.key]
|
||||
} else {
|
||||
val newSubsRaw =
|
||||
withContext(Dispatchers.IO) { SubsItem.getSubscriptionRaw(entry.key) }
|
||||
newSubsRaw?.apps?.apply {
|
||||
updateAppInfo(*map { a -> a.id }.toTypedArray())
|
||||
}
|
||||
entry.key to newSubsRaw
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user