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:
Mapp/src/main/kotlin/me/rhunk/snapenhance/storage/Messaging.kt | 6++++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt | 2++
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/features/FeaturesRootSection.kt | 11++++++++---
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/features/ManageRuleFeature.kt | 216+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/src/main/assets/lang/en_US.json | 13+++++++++++++
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/ModConfig.kt | 8++++----
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))