EditRule.kt (21761B) - raw


      1 package me.rhunk.snapenhance.ui.manager.pages.tracker
      2 
      3 import androidx.compose.foundation.clickable
      4 import androidx.compose.foundation.layout.*
      5 import androidx.compose.foundation.lazy.LazyColumn
      6 import androidx.compose.foundation.lazy.items
      7 import androidx.compose.foundation.rememberScrollState
      8 import androidx.compose.foundation.verticalScroll
      9 import androidx.compose.material.icons.Icons
     10 import androidx.compose.material.icons.filled.*
     11 import androidx.compose.material3.*
     12 import androidx.compose.runtime.*
     13 import androidx.compose.ui.Alignment
     14 import androidx.compose.ui.Modifier
     15 import androidx.compose.ui.draw.clip
     16 import androidx.compose.ui.graphics.Color
     17 import androidx.compose.ui.text.TextStyle
     18 import androidx.compose.ui.text.font.FontWeight
     19 import androidx.compose.ui.text.style.TextAlign
     20 import androidx.compose.ui.text.style.TextOverflow
     21 import androidx.compose.ui.unit.dp
     22 import androidx.compose.ui.unit.sp
     23 import androidx.navigation.NavBackStackEntry
     24 import me.rhunk.snapenhance.common.data.*
     25 import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState
     26 import me.rhunk.snapenhance.common.ui.rememberAsyncMutableStateList
     27 import me.rhunk.snapenhance.storage.*
     28 import me.rhunk.snapenhance.ui.manager.Routes
     29 import me.rhunk.snapenhance.ui.manager.pages.social.AddFriendDialog
     30 
     31 @Composable
     32 fun ActionCheckbox(
     33     text: String,
     34     checked: MutableState<Boolean>,
     35     onChanged: (Boolean) -> Unit = {}
     36 ) {
     37     Row(
     38         modifier = Modifier.clickable {
     39             checked.value = !checked.value
     40             onChanged(checked.value)
     41         },
     42         horizontalArrangement = Arrangement.spacedBy(2.dp),
     43         verticalAlignment = Alignment.CenterVertically
     44     ) {
     45         Checkbox(
     46             modifier = Modifier.size(30.dp),
     47             checked = checked.value,
     48             onCheckedChange = {
     49                 checked.value = it
     50                 onChanged(it)
     51             }
     52         )
     53         Text(text, fontSize = 12.sp)
     54     }
     55 }
     56 
     57 
     58 @Composable
     59 fun ConditionCheckboxes(
     60     params: TrackerRuleActionParams
     61 ) {
     62     ActionCheckbox(text = "Only when I'm inside conversation", checked = remember { mutableStateOf(params.onlyInsideConversation) }, onChanged = { params.onlyInsideConversation = it })
     63     ActionCheckbox(text = "Only when I'm outside conversation", checked = remember { mutableStateOf(params.onlyOutsideConversation) }, onChanged = { params.onlyOutsideConversation = it })
     64     ActionCheckbox(text = "Only when Snapchat is active", checked = remember { mutableStateOf(params.onlyWhenAppActive) }, onChanged = { params.onlyWhenAppActive = it })
     65     ActionCheckbox(text = "Only when Snapchat is inactive", checked = remember { mutableStateOf(params.onlyWhenAppInactive) }, onChanged = { params.onlyWhenAppInactive = it })
     66     ActionCheckbox(text = "No notification when Snapchat is active", checked = remember { mutableStateOf(params.noPushNotificationWhenAppActive) }, onChanged = { params.noPushNotificationWhenAppActive = it })
     67 }
     68 
     69 class EditRule : Routes.Route() {
     70     private val fab = mutableStateOf<@Composable (() -> Unit)?>(null)
     71 
     72     // persistent add event state
     73     private var currentEventType by mutableStateOf(TrackerEventType.CONVERSATION_ENTER.key)
     74     private var addEventActions by mutableStateOf(emptySet<TrackerRuleAction>())
     75     private val addEventActionParams by mutableStateOf(TrackerRuleActionParams())
     76 
     77     override val floatingActionButton: @Composable () -> Unit = {
     78         fab.value?.invoke()
     79     }
     80 
     81     @OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
     82     override val content: @Composable (NavBackStackEntry) -> Unit = { navBackStackEntry ->
     83         val currentRuleId = navBackStackEntry.arguments?.getString("rule_id")?.toIntOrNull()
     84 
     85         val events = rememberAsyncMutableStateList(defaultValue = emptyList()) {
     86             currentRuleId?.let { ruleId ->
     87                 context.database.getTrackerEvents(ruleId)
     88             } ?: emptyList()
     89         }
     90         var currentScopeType by remember { mutableStateOf(TrackerScopeType.BLACKLIST) }
     91         val scopes = rememberAsyncMutableStateList(defaultValue = emptyList()) {
     92             currentRuleId?.let { ruleId ->
     93                 context.database.getRuleTrackerScopes(ruleId).also {
     94                     currentScopeType = if (it.isEmpty()) {
     95                         TrackerScopeType.WHITELIST
     96                     } else {
     97                         it.values.first()
     98                     }
     99                 }.map { it.key }
    100             } ?: emptyList()
    101         }
    102         val ruleName = rememberAsyncMutableState(defaultValue = "", keys = arrayOf(currentRuleId)) {
    103             currentRuleId?.let { ruleId ->
    104                 context.database.getTrackerRule(ruleId)?.name ?: "Custom Rule"
    105             } ?: "Custom Rule"
    106         }
    107 
    108         LaunchedEffect(Unit) {
    109             fab.value = {
    110                 var deleteConfirmation by remember { mutableStateOf(false) }
    111 
    112                 if (deleteConfirmation) {
    113                     AlertDialog(
    114                         onDismissRequest = { deleteConfirmation = false },
    115                         title = { Text("Delete Rule") },
    116                         text = { Text("Are you sure you want to delete this rule?") },
    117                         confirmButton = {
    118                             Button(
    119                                 onClick = {
    120                                     if (currentRuleId != null) {
    121                                         context.database.deleteTrackerRule(currentRuleId)
    122                                     }
    123                                     routes.navController.popBackStack()
    124                                 }
    125                             ) {
    126                                 Text("Delete")
    127                             }
    128                         },
    129                         dismissButton = {
    130                             Button(
    131                                 onClick = { deleteConfirmation = false }
    132                             ) {
    133                                 Text("Cancel")
    134                             }
    135                         }
    136                     )
    137                 }
    138 
    139                 Column(
    140                     modifier = Modifier.fillMaxWidth(),
    141                     verticalArrangement = Arrangement.spacedBy(6.dp),
    142                     horizontalAlignment = Alignment.End
    143                 ) {
    144                     ExtendedFloatingActionButton(
    145                         onClick = {
    146                             val ruleId = currentRuleId ?: context.database.newTrackerRule()
    147                             events.forEach { event ->
    148                                 context.database.addOrUpdateTrackerRuleEvent(
    149                                     event.id.takeIf { it > -1 },
    150                                     ruleId,
    151                                     event.eventType,
    152                                     event.params,
    153                                     event.actions
    154                                 )
    155                             }
    156                             context.database.setTrackerRuleName(ruleId, ruleName.value.trim())
    157                             context.database.setRuleTrackerScopes(ruleId, currentScopeType, scopes)
    158                             routes.navController.popBackStack()
    159                         },
    160                         text = { Text("Save Rule") },
    161                         icon = { Icon(Icons.Default.Save, contentDescription = "Save Rule") }
    162                     )
    163 
    164                     if (currentRuleId != null) {
    165                         ExtendedFloatingActionButton(
    166                             containerColor = MaterialTheme.colorScheme.error,
    167                             onClick = { deleteConfirmation = true },
    168                             text = { Text("Delete Rule") },
    169                             icon = { Icon(Icons.Default.DeleteOutline, contentDescription = "Delete Rule") }
    170                         )
    171                     }
    172                 }
    173             }
    174         }
    175 
    176         DisposableEffect(Unit) {
    177             onDispose { fab.value = null }
    178         }
    179 
    180         LazyColumn(
    181             verticalArrangement = Arrangement.spacedBy(2.dp),
    182         ) {
    183             item {
    184                 TextField(
    185                     value = ruleName.value,
    186                     onValueChange = {
    187                         ruleName.value = it
    188                     },
    189                     singleLine = true,
    190                     placeholder = {
    191                         Text(
    192                             "Rule Name",
    193                             fontSize = 18.sp,
    194                             modifier = Modifier.fillMaxWidth(),
    195                             textAlign = TextAlign.Center
    196                         )
    197                     },
    198                     modifier = Modifier.fillMaxWidth(),
    199                     colors = TextFieldDefaults.colors(
    200                         focusedContainerColor = Color.Transparent,
    201                         unfocusedContainerColor = Color.Transparent
    202                     ),
    203                     textStyle = TextStyle(fontSize = 20.sp, textAlign = TextAlign.Center, fontWeight = FontWeight.Bold)
    204                 )
    205             }
    206 
    207 
    208             item {
    209                 Column(
    210                     modifier = Modifier.fillMaxWidth(),
    211                 ){
    212                     Text("Scope", fontSize = 16.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(16.dp))
    213 
    214                     var addFriendDialog by remember { mutableStateOf(null as AddFriendDialog?) }
    215 
    216                     val friendDialogActions = remember {
    217                         AddFriendDialog.Actions(
    218                             onFriendState = { friend, state ->
    219                                 if (state) {
    220                                     scopes.add(friend.userId)
    221                                 } else {
    222                                     scopes.remove(friend.userId)
    223                                 }
    224                             },
    225                             onGroupState = { group, state ->
    226                                 if (state) {
    227                                     scopes.add(group.conversationId)
    228                                 } else {
    229                                     scopes.remove(group.conversationId)
    230                                 }
    231                             },
    232                             getFriendState = { friend ->
    233                                 friend.userId in scopes
    234                             },
    235                             getGroupState = { group ->
    236                                 group.conversationId in scopes
    237                             }
    238                         )
    239                     }
    240 
    241                     Box(modifier = Modifier.clickable { scopes.clear() }) {
    242                         Row(
    243                             modifier = Modifier
    244                                 .fillMaxWidth()
    245                                 .padding(10.dp),
    246                             horizontalArrangement = Arrangement.spacedBy(4.dp),
    247                             verticalAlignment = Alignment.CenterVertically,
    248                         ) {
    249                             RadioButton(selected = scopes.isEmpty(), onClick = null)
    250                             Text("All Friends/Groups")
    251                         }
    252                     }
    253 
    254                     Box(modifier = Modifier.clickable {
    255                         currentScopeType = TrackerScopeType.WHITELIST
    256                         addFriendDialog = AddFriendDialog(
    257                             context,
    258                             friendDialogActions,
    259                             pinnedIds = scopes,
    260                         )
    261                     }) {
    262                         Row(
    263                             modifier = Modifier
    264                                 .fillMaxWidth()
    265                                 .padding(10.dp),
    266                             horizontalArrangement = Arrangement.spacedBy(4.dp),
    267                             verticalAlignment = Alignment.CenterVertically,
    268                         ) {
    269                             RadioButton(selected = scopes.isNotEmpty() && currentScopeType == TrackerScopeType.WHITELIST, onClick = null)
    270                             Text("No one except " + if (currentScopeType == TrackerScopeType.WHITELIST && scopes.isNotEmpty()) scopes.size.toString() + " friends/groups" else "...")
    271                         }
    272                     }
    273 
    274                     Box(modifier = Modifier.clickable {
    275                         currentScopeType = TrackerScopeType.BLACKLIST
    276                         addFriendDialog = AddFriendDialog(
    277                             context,
    278                             friendDialogActions,
    279                             pinnedIds = scopes,
    280                         )
    281                     }) {
    282                         Row(
    283                             modifier = Modifier
    284                                 .fillMaxWidth()
    285                                 .padding(10.dp),
    286                             horizontalArrangement = Arrangement.spacedBy(4.dp),
    287                             verticalAlignment = Alignment.CenterVertically,
    288                         ) {
    289                             RadioButton(selected = scopes.isNotEmpty() && currentScopeType == TrackerScopeType.BLACKLIST, onClick = null)
    290                             Text("Everyone except " + if (currentScopeType == TrackerScopeType.BLACKLIST && scopes.isNotEmpty()) scopes.size.toString() + " friends/groups" else "...")
    291                         }
    292                     }
    293 
    294                     addFriendDialog?.Content {
    295                         addFriendDialog = null
    296                     }
    297                 }
    298 
    299                 var addEventDialog by remember { mutableStateOf(false) }
    300                 val showDropdown = remember { mutableStateOf(false) }
    301 
    302                 Row(
    303                     modifier = Modifier.fillMaxWidth(),
    304                     horizontalArrangement = Arrangement.SpaceBetween,
    305                     verticalAlignment = Alignment.CenterVertically
    306                 ) {
    307                     Text("Events", fontSize = 16.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(16.dp))
    308                     IconButton(onClick = { addEventDialog = true }, modifier = Modifier.padding(8.dp)) {
    309                         Icon(Icons.Default.Add, contentDescription = "Add Event", modifier = Modifier.size(32.dp))
    310                     }
    311                 }
    312 
    313                 if (addEventDialog) {
    314                     AlertDialog(
    315                         onDismissRequest = { addEventDialog = false },
    316                         title = { Text("Add Event", fontSize = 20.sp, fontWeight = FontWeight.Bold) },
    317                         text = {
    318                             Column(
    319                                 modifier = Modifier
    320                                     .verticalScroll(rememberScrollState())
    321                                     .padding(16.dp),
    322                                 verticalArrangement = Arrangement.spacedBy(4.dp)
    323                             ) {
    324                                 Row(
    325                                     modifier = Modifier
    326                                         .fillMaxWidth()
    327                                         .padding(2.dp),
    328                                     horizontalArrangement = Arrangement.SpaceBetween,
    329                                     verticalAlignment = Alignment.CenterVertically
    330                                 ) {
    331                                     Text("Type", fontSize = 14.sp, fontWeight = FontWeight.Bold)
    332                                     ExposedDropdownMenuBox(expanded = showDropdown.value, onExpandedChange = { showDropdown.value = it }) {
    333                                         ElevatedButton(
    334                                             onClick = { showDropdown.value = true },
    335                                             modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable)
    336                                         ) {
    337                                             Text(currentEventType, overflow = TextOverflow.Ellipsis, maxLines = 1)
    338                                         }
    339                                         DropdownMenu(expanded = showDropdown.value, onDismissRequest = { showDropdown.value = false }) {
    340                                             TrackerEventType.entries.forEach { eventType ->
    341                                                 DropdownMenuItem(onClick = {
    342                                                     currentEventType = eventType.key
    343                                                     showDropdown.value = false
    344                                                 }, text = {
    345                                                     Text(eventType.key)
    346                                                 })
    347                                             }
    348                                         }
    349                                     }
    350                                 }
    351 
    352                                 Text("Triggers", fontSize = 14.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(2.dp))
    353 
    354                                 FlowRow(
    355                                     modifier = Modifier
    356                                         .fillMaxWidth()
    357                                         .padding(2.dp),
    358                                 ) {
    359                                     TrackerRuleAction.entries.forEach { action ->
    360                                         ActionCheckbox(action.name, checked = remember { mutableStateOf(addEventActions.contains(action)) }) {
    361                                             if (it) {
    362                                                 addEventActions += action
    363                                             } else {
    364                                                 addEventActions -= action
    365                                             }
    366                                         }
    367                                     }
    368                                 }
    369 
    370                                 Text("Conditions", fontSize = 14.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(2.dp))
    371                                 ConditionCheckboxes(addEventActionParams)
    372                             }
    373                         },
    374                         confirmButton = {
    375                             Button(
    376                                 onClick = {
    377                                     events.add(0, TrackerRuleEvent(-1, true, currentEventType, addEventActionParams.copy(), addEventActions.toList()))
    378                                     addEventDialog = false
    379                                 }
    380                             ) {
    381                                 Text("Add")
    382                             }
    383                         }
    384                     )
    385                 }
    386             }
    387 
    388             item {
    389                 if (events.isEmpty()) {
    390                     Text("No events", fontSize = 12.sp, fontWeight = FontWeight.Light, modifier = Modifier
    391                         .padding(10.dp)
    392                         .fillMaxWidth(), textAlign = TextAlign.Center)
    393                 }
    394             }
    395 
    396             items(events) { event ->
    397                 var expanded by remember { mutableStateOf(false) }
    398 
    399                 ElevatedCard(
    400                     modifier = Modifier
    401                         .fillMaxWidth()
    402                         .clip(MaterialTheme.shapes.medium)
    403                         .padding(4.dp),
    404                     onClick = { expanded = !expanded }
    405                 ) {
    406                     Column {
    407                         Row(
    408                             modifier = Modifier
    409                                 .fillMaxWidth()
    410                                 .padding(10.dp),
    411                             horizontalArrangement = Arrangement.SpaceBetween,
    412                             verticalAlignment = Alignment.CenterVertically
    413                         ) {
    414                             Row(
    415                                 modifier = Modifier.weight(1f, fill = false),
    416                                 horizontalArrangement = Arrangement.spacedBy(6.dp),
    417                                 verticalAlignment = Alignment.CenterVertically,
    418                             ) {
    419                                 Icon(
    420                                     if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
    421                                     contentDescription = null,
    422                                     modifier = Modifier.size(24.dp)
    423                                 )
    424                                 Column {
    425                                     Text(event.eventType, lineHeight = 20.sp, fontSize = 18.sp, fontWeight = FontWeight.Bold)
    426                                     Text(text = event.actions.joinToString(", ") { it.name }, fontSize = 10.sp, fontWeight = FontWeight.Light, overflow = TextOverflow.Ellipsis, maxLines = 1, lineHeight = 14.sp)
    427                                 }
    428                             }
    429                             OutlinedIconButton(
    430                                 onClick = {
    431                                     if (event.id > -1) {
    432                                         context.database.deleteTrackerRuleEvent(event.id)
    433                                     }
    434                                     events.remove(event)
    435                                 }
    436                             ) {
    437                                 Icon(Icons.Default.DeleteOutline, contentDescription = "Delete")
    438                             }
    439                         }
    440                         if (expanded) {
    441                             Column(
    442                                 modifier = Modifier.padding(10.dp)
    443                             ) {
    444                                 ConditionCheckboxes(event.params)
    445                             }
    446                         }
    447                     }
    448                 }
    449             }
    450 
    451             item {
    452                 Spacer(modifier = Modifier.height(140.dp))
    453             }
    454         }
    455     }
    456 }
    457