feat(ui/features): better rule list management
This commit is contained in:
@@ -179,3 +179,9 @@ fun AppDatabase.getRuleIds(type: String): MutableList<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fun AppDatabase.clearRuleIds(type: String) {
|
||||
executeAsync {
|
||||
database.execSQL("DELETE FROM rules WHERE type = ?", arrayOf(type))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import me.rhunk.snapenhance.ui.manager.pages.social.MessagingPreview
|
||||
import me.rhunk.snapenhance.ui.manager.pages.social.SocialRootSection
|
||||
import me.rhunk.snapenhance.ui.manager.pages.theming.EditThemeSection
|
||||
import me.rhunk.snapenhance.ui.manager.pages.ManageReposSection
|
||||
import me.rhunk.snapenhance.ui.manager.pages.features.ManageRuleFeature
|
||||
import me.rhunk.snapenhance.ui.manager.pages.theming.ThemingRoot
|
||||
import me.rhunk.snapenhance.ui.manager.pages.tracker.EditRule
|
||||
import me.rhunk.snapenhance.ui.manager.pages.tracker.FriendTrackerManagerRoot
|
||||
@@ -52,6 +53,7 @@ class Routes(
|
||||
val tasks = route(RouteInfo("tasks", icon = Icons.Default.TaskAlt, primary = true), TasksRootSection())
|
||||
|
||||
val features = route(RouteInfo("features", icon = Icons.Default.Stars, primary = true), FeaturesRootSection())
|
||||
val manageRuleFeature = route(RouteInfo("manage_rule_feature/?rule_type={rule_type}"), ManageRuleFeature()).parent(features)
|
||||
|
||||
val home = route(RouteInfo("home", icon = Icons.Default.Home, primary = true), HomeRootSection())
|
||||
val settings = route(RouteInfo("home_settings"), HomeSettings()).parent(home)
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.net.Uri
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -19,7 +18,6 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -33,7 +31,6 @@ import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
import com.github.skydoves.colorpicker.compose.AlphaTile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -444,6 +441,14 @@ class FeaturesRootSection : Routes.Route() {
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
PropertyAction(property, registerClickCallback = { callback ->
|
||||
if (property.key.propertyTranslationPath().startsWith("rules.properties")) {
|
||||
clickCallback = {
|
||||
routes.manageRuleFeature.navigate {
|
||||
put("rule_type", property.key.name)
|
||||
}
|
||||
}
|
||||
return@PropertyAction clickCallback!!
|
||||
}
|
||||
clickCallback = callback
|
||||
callback
|
||||
})
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package me.rhunk.snapenhance.ui.manager.pages.features
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.common.data.MessagingRuleType
|
||||
import me.rhunk.snapenhance.common.data.RuleState
|
||||
import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState
|
||||
import me.rhunk.snapenhance.common.ui.rememberAsyncUpdateDispatcher
|
||||
import me.rhunk.snapenhance.storage.clearRuleIds
|
||||
import me.rhunk.snapenhance.storage.getRuleIds
|
||||
import me.rhunk.snapenhance.storage.setRule
|
||||
import me.rhunk.snapenhance.ui.manager.Routes
|
||||
import me.rhunk.snapenhance.ui.manager.pages.social.AddFriendDialog
|
||||
import me.rhunk.snapenhance.ui.manager.pages.social.AddFriendDialog.Actions
|
||||
import me.rhunk.snapenhance.ui.util.AlertDialogs
|
||||
import me.rhunk.snapenhance.ui.util.Dialog
|
||||
|
||||
class ManageRuleFeature : Routes.Route() {
|
||||
@Composable
|
||||
fun SelectRuleTypeRadio(
|
||||
checked: Boolean,
|
||||
text: String,
|
||||
onStateChanged: (Boolean) -> Unit,
|
||||
selectedBlock: @Composable () -> Unit = {},
|
||||
) {
|
||||
Box(modifier = Modifier.clickable {
|
||||
onStateChanged(!checked)
|
||||
}) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(selected = checked, onClick = null)
|
||||
Text(text)
|
||||
}
|
||||
if (checked) {
|
||||
Column(modifier = Modifier
|
||||
.offset(x = 15.dp)
|
||||
.padding(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
selectedBlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val content: @Composable (NavBackStackEntry) -> Unit = content@{ navBackStackEntry ->
|
||||
val currentRuleType = navBackStackEntry.arguments?.getString("rule_type")?.let {
|
||||
MessagingRuleType.getByName(it)
|
||||
} ?: return@content
|
||||
|
||||
var ruleState by remember {
|
||||
mutableStateOf(context.config.root.rules.getRuleState(currentRuleType))
|
||||
}
|
||||
|
||||
val propertyKeyPair = remember {
|
||||
context.config.root.rules.getPropertyPair(currentRuleType.key)
|
||||
}
|
||||
|
||||
val updateDispatcher = rememberAsyncUpdateDispatcher()
|
||||
val currentRuleIds by rememberAsyncMutableState(defaultValue = mutableListOf(), updateDispatcher = updateDispatcher) {
|
||||
context.database.getRuleIds(currentRuleType.key)
|
||||
}
|
||||
|
||||
fun setRuleState(newState: RuleState?) {
|
||||
ruleState = newState
|
||||
propertyKeyPair.value.setAny(newState?.key)
|
||||
context.coroutineScope.launch {
|
||||
context.config.writeConfig(dispatchConfigListener = false)
|
||||
}
|
||||
}
|
||||
|
||||
var addFriendDialog by remember { mutableStateOf(null as AddFriendDialog?) }
|
||||
|
||||
LaunchedEffect(addFriendDialog) {
|
||||
if (addFriendDialog == null) {
|
||||
updateDispatcher.dispatch()
|
||||
}
|
||||
}
|
||||
|
||||
fun showAddFriendDialog() {
|
||||
addFriendDialog = AddFriendDialog(
|
||||
context = context,
|
||||
pinnedIds = currentRuleIds,
|
||||
actionHandler = Actions(
|
||||
onFriendState = { friend, state ->
|
||||
context.database.setRule(friend.userId, currentRuleType.key, state)
|
||||
if (state) {
|
||||
currentRuleIds.add(friend.userId)
|
||||
} else {
|
||||
currentRuleIds.remove(friend.userId)
|
||||
}
|
||||
},
|
||||
onGroupState = { group, state ->
|
||||
context.database.setRule(group.conversationId, currentRuleType.key, state)
|
||||
if (state) {
|
||||
currentRuleIds.add(group.conversationId)
|
||||
} else {
|
||||
currentRuleIds.remove(group.conversationId)
|
||||
}
|
||||
},
|
||||
getFriendState = { friend ->
|
||||
currentRuleIds.contains(friend.userId)
|
||||
},
|
||||
getGroupState = { group ->
|
||||
currentRuleIds.contains(group.conversationId)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (addFriendDialog != null) {
|
||||
addFriendDialog?.Content {
|
||||
addFriendDialog = null
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = remember {
|
||||
context.translation[propertyKeyPair.key.propertyName()]
|
||||
},
|
||||
fontSize = 20.sp,
|
||||
)
|
||||
Text(
|
||||
text = remember {
|
||||
context.translation[propertyKeyPair.key.propertyDescription()]
|
||||
},
|
||||
fontWeight = FontWeight.Light,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
)
|
||||
}
|
||||
|
||||
SelectRuleTypeRadio(checked = ruleState == null, text = translation["disable_state_option"], onStateChanged = {
|
||||
setRuleState(null)
|
||||
}) {
|
||||
Text(text = translation["disable_state_subtext"], fontWeight = FontWeight.Light, fontSize = 12.sp)
|
||||
}
|
||||
SelectRuleTypeRadio(checked = ruleState == RuleState.WHITELIST, text = translation["whitelist_state_option"], onStateChanged = {
|
||||
setRuleState(RuleState.WHITELIST)
|
||||
}) {
|
||||
Text(text = translation.format("whitelist_state_subtext", "count" to currentRuleIds.size.toString()), fontWeight = FontWeight.Light, fontSize = 12.sp)
|
||||
OutlinedButton(onClick = {
|
||||
showAddFriendDialog()
|
||||
}) {
|
||||
Text(text = translation["whitelist_state_button"])
|
||||
}
|
||||
}
|
||||
SelectRuleTypeRadio(checked = ruleState == RuleState.BLACKLIST, text = translation["blacklist_state_option"], onStateChanged = {
|
||||
setRuleState(RuleState.BLACKLIST)
|
||||
}) {
|
||||
Text(text = translation.format("blacklist_state_subtext", "count" to currentRuleIds.size.toString()), fontWeight = FontWeight.Light, fontSize = 12.sp)
|
||||
OutlinedButton(onClick = { showAddFriendDialog() }) {
|
||||
Text(text = translation["blacklist_state_button"])
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(5.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
var confirmationDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (confirmationDialog) {
|
||||
Dialog(onDismissRequest = {
|
||||
confirmationDialog = false
|
||||
}) {
|
||||
remember { AlertDialogs(context.translation) }.ConfirmDialog(
|
||||
title = translation["dialog_clear_confirmation_text"],
|
||||
onDismiss = { confirmationDialog = false },
|
||||
onConfirm = {
|
||||
context.database.clearRuleIds(currentRuleType.key)
|
||||
context.coroutineScope.launch(context.database.executor.asCoroutineDispatcher()) {
|
||||
updateDispatcher.dispatch()
|
||||
}
|
||||
confirmationDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button(onClick = { confirmationDialog = true }) {
|
||||
Text(text = translation["clear_list_button"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
"routes": {
|
||||
"tasks": "Tasks",
|
||||
"features": "Features",
|
||||
"manage_rule_feature": "Manage Rule Feature",
|
||||
"home": "Home",
|
||||
"home_settings": "Settings",
|
||||
"home_logs": "Logs",
|
||||
@@ -95,6 +96,18 @@
|
||||
"config_export_failure_toast": "Failed to export config {error}",
|
||||
"saved_config_snackbar": "Config saved"
|
||||
},
|
||||
"manage_rule_feature": {
|
||||
"disable_state_option": "Disabled",
|
||||
"disable_state_subtext": "No friends/groups will be affected",
|
||||
"whitelist_state_option": "No one except ...",
|
||||
"whitelist_state_subtext": "Only {count} friends/groups will be affected by this rule",
|
||||
"whitelist_state_button": "Select allowed friends/groups",
|
||||
"blacklist_state_option": "Everyone except ...",
|
||||
"blacklist_state_subtext": "Everyone except {count} friends/groups will be affected by this rule",
|
||||
"blacklist_state_button": "Select excluded friends/groups",
|
||||
"clear_list_button": "Clear friends/groups list",
|
||||
"dialog_clear_confirmation_text": "Are you sure you want to clear the list?"
|
||||
},
|
||||
"social": {
|
||||
"friends_tab": "Friends",
|
||||
"groups_tab": "Groups",
|
||||
|
||||
@@ -70,11 +70,11 @@ class ModConfig(
|
||||
}
|
||||
}
|
||||
|
||||
fun writeConfig() {
|
||||
writeConfigObject(root)
|
||||
fun writeConfig(dispatchConfigListener: Boolean = true) {
|
||||
writeConfigObject(root, dispatchConfigListener)
|
||||
}
|
||||
|
||||
private fun writeConfigObject(config: RootConfig) {
|
||||
private fun writeConfigObject(config: RootConfig, dispatchConfigListener: Boolean = true) {
|
||||
var shouldRestart = false
|
||||
var shouldCleanCache = false
|
||||
var configChanged = false
|
||||
@@ -113,7 +113,7 @@ class ModConfig(
|
||||
val oldConfig = runCatching { fileWrapper.readBytes().toString(Charsets.UTF_8) }.getOrNull()
|
||||
fileWrapper.writeBytes(exportToString(config = config).toByteArray(Charsets.UTF_8))
|
||||
|
||||
configStateListener?.takeIf { it.asBinder().pingBinder() }?.also {
|
||||
configStateListener?.takeIf { dispatchConfigListener && it.asBinder().pingBinder() }?.also {
|
||||
runCatching {
|
||||
compareDiff(createRootConfig().apply {
|
||||
fromJson(gson.fromJson(oldConfig ?: return@runCatching, JsonObject::class.java))
|
||||
|
||||
Reference in New Issue
Block a user