commit 4325e512ca45dce3186ea7a6fb2811d3d4c0fd81
parent 324eae8bfa50475c6624f482fbe5b23de35290b4
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Tue, 13 Aug 2024 14:58:31 +0200
feat(ui/features): better rule list management
Diffstat:
6 files changed, 249 insertions(+), 7 deletions(-)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/storage/Messaging.kt b/app/src/main/kotlin/me/rhunk/snapenhance/storage/Messaging.kt
@@ -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))
+ }
+}
+
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt
@@ -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)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/features/FeaturesRootSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/features/FeaturesRootSection.kt
@@ -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
})
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/features/ManageRuleFeature.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/features/ManageRuleFeature.kt
@@ -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"])
+ }
+ }
+ }
+ }
+}+
\ No newline at end of file
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -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",
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/ModConfig.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/ModConfig.kt
@@ -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))