feat: 规则分类

This commit is contained in:
lisonge 2023-12-14 18:05:41 +08:00
parent 7ad5917f71
commit 606dafe4c1
17 changed files with 980 additions and 135 deletions

View 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')"
]
}
}

View 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>>
}
}

View File

@ -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>>

View File

@ -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
}

View File

@ -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
}

View File

@ -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() {

View File

@ -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 },

View File

@ -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)

View 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
)
}
}
}
}
}

View 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())
}

View File

@ -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,
)

View File

@ -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

View File

@ -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 }

View File

@ -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())

View File

@ -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 }
)
}
}

View File

@ -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,
)

View File

@ -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()
}
}
}