From 7703d3f007dc661624de1c2c6786d51132add223 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Wed, 23 Aug 2023 16:19:31 +0200 Subject: [PATCH] feat: better friend add list - alert dialogs --- .../sections/features/FeaturesSection.kt | 9 +- .../sections/social/AddFriendDialog.kt | 206 +++++++++++++++--- .../manager/sections/social/ScopeContent.kt | 28 +-- .../manager/sections/social/SocialSection.kt | 41 +++- .../ui/setup/screens/impl/MappingsScreen.kt | 22 +- .../Dialogs.kt => util/AlertDialogs.kt} | 68 +++++- 6 files changed, 299 insertions(+), 75 deletions(-) rename app/src/main/kotlin/me/rhunk/snapenhance/ui/{manager/sections/features/Dialogs.kt => util/AlertDialogs.kt} (79%) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt index dbd5258a..6f86b8a2 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt @@ -81,13 +81,14 @@ import me.rhunk.snapenhance.core.config.PropertyPair import me.rhunk.snapenhance.core.config.PropertyValue import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper +import me.rhunk.snapenhance.ui.util.AlertDialogs import me.rhunk.snapenhance.ui.util.chooseFolder import me.rhunk.snapenhance.ui.util.openFile import me.rhunk.snapenhance.ui.util.saveFile @OptIn(ExperimentalMaterial3Api::class) class FeaturesSection : Section() { - private val dialogs by lazy { Dialogs(context.translation) } + private val alertDialogs by lazy { AlertDialogs(context.translation) } companion object { const val MAIN_ROUTE = "feature_root" @@ -217,7 +218,7 @@ class FeaturesSection : Section() { registerDialogOnClickCallback() dialogComposable = { - dialogs.UniqueSelectionDialog(property) + alertDialogs.UniqueSelectionDialog(property) } Text( @@ -234,10 +235,10 @@ class FeaturesSection : Section() { dialogComposable = { when (dataType) { DataProcessors.Type.STRING_MULTIPLE_SELECTION -> { - dialogs.MultipleSelectionDialog(property) + alertDialogs.MultipleSelectionDialog(property) } DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> { - dialogs.KeyboardInputDialog(property) { showDialog = false } + alertDialogs.KeyboardInputDialog(property) { showDialog = false } } else -> {} } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt index 7a4531e6..79525c0b 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt @@ -1,26 +1,41 @@ package me.rhunk.snapenhance.ui.manager.sections.social import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -37,16 +52,73 @@ class AddFriendDialog( private val context: RemoteSideContext, private val section: SocialSection, ) { + @Composable + private fun ListCardEntry(name: String, exists: Boolean, stateChanged: (state: Boolean) -> Unit = { }) { + var state by remember { mutableStateOf(exists) } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + state = !state + stateChanged(state) + } + .padding(4.dp), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically + ) { + Text( + text = name, + fontSize = 15.sp, + modifier = Modifier + .weight(1f) + ) + + androidx.compose.material3.Checkbox( + checked = state, + onCheckedChange = { + state = it + stateChanged(state) + } + ) + } + } @Composable - private fun ListCardEntry(name: String, modifier: Modifier = Modifier) { - Card( + private fun DialogHeader(searchKeyword: MutableState) { + Column( modifier = Modifier - .padding(5.dp) - .then(modifier), + .fillMaxWidth() + .padding(10.dp), ) { - Text(text = name, modifier = Modifier.padding(10.dp)) + Text( + text = "Add Friend or Group", + fontSize = 23.sp, + fontWeight = FontWeight.ExtraBold, + modifier = Modifier + .align(alignment = Alignment.CenterHorizontally) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + TextField( + value = searchKeyword.value, + onValueChange = { searchKeyword.value = it }, + label = { + Text(text = "Search") + }, + modifier = Modifier + .weight(1f) + .padding(end = 10.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), + leadingIcon = { + Icon(Icons.Filled.Search, contentDescription = "Search") + } + ) } } @@ -84,52 +156,118 @@ class AddFriendDialog( } } - Dialog(onDismissRequest = { - timeoutJob?.cancel() - dismiss() - }) { - Card { - if (hasFetchError) { - Text(text = "Failed to load friends and groups. Make sure Snapchat is installed and logged in.") - return@Card - } + Dialog( + onDismissRequest = { + timeoutJob?.cancel() + dismiss() + }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Card( + colors = CardDefaults.elevatedCardColors(), + modifier = Modifier + .fillMaxSize() + .fillMaxWidth() + .padding(all = 20.dp) + ) { if (cachedGroups == null || cachedFriends == null) { - CircularProgressIndicator( + Column( modifier = Modifier - .padding() - .size(30.dp), - strokeWidth = 3.dp, - color = MaterialTheme.colorScheme.onPrimary - ) + .fillMaxSize() + .padding(10.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (hasFetchError) { + Text( + text = "Failed to fetch data", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 10.dp, top = 10.dp) + ) + return@Card + } + CircularProgressIndicator( + modifier = Modifier + .padding() + .size(30.dp), + strokeWidth = 3.dp, + color = MaterialTheme.colorScheme.primary + ) + } return@Card } + val searchKeyword = remember { mutableStateOf("") } + + val filteredGroups = cachedGroups!!.takeIf { searchKeyword.value.isNotBlank() }?.filter { + it.name.contains(searchKeyword.value, ignoreCase = true) + } ?: cachedGroups!! + + val filteredFriends = cachedFriends!!.takeIf { searchKeyword.value.isNotBlank() }?.filter { + it.mutableUsername.contains(searchKeyword.value, ignoreCase = true) || + it.displayName?.contains(searchKeyword.value, ignoreCase = true) == true + } ?: cachedFriends!! + + DialogHeader(searchKeyword) + LazyColumn( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .padding(10.dp) ) { item { - Text(text = "Groups", fontSize = 20.sp) - Spacer(modifier = Modifier.padding(5.dp)) + if (filteredGroups.isEmpty()) return@item + Text(text = "Groups", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 10.dp, top = 10.dp) + ) } - items(cachedGroups!!.size) { - ListCardEntry(name = cachedGroups!![it].name, modifier = Modifier.clickable { - context.bridgeService.triggerGroupSync(cachedGroups!![it].conversationId) + + items(filteredGroups.size) { + val group = filteredGroups[it] + + ListCardEntry( + name = group.name, + exists = remember { context.modDatabase.getGroupInfo(group.conversationId) != null } + ) { state -> + if (state) { + context.bridgeService.triggerGroupSync(cachedGroups!![it].conversationId) + } else { + context.modDatabase.deleteGroup(group.conversationId) + } context.modDatabase.executeAsync { section.onResumed() } - }) + } } + item { - Text(text = "Friends", fontSize = 20.sp) - Spacer(modifier = Modifier.padding(5.dp)) + if (filteredFriends.isEmpty()) return@item + Text(text = "Friends", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 10.dp, top = 10.dp) + ) } - items(cachedFriends!!.size) { - ListCardEntry(name = cachedFriends!![it].displayName ?: cachedFriends!![it].mutableUsername, modifier = Modifier.clickable { - context.bridgeService.triggerFriendSync(cachedFriends!![it].userId) + + items(filteredFriends.size) { + val friend = filteredFriends[it] + + ListCardEntry( + name = friend.displayName?.takeIf { name -> name.isNotBlank() } ?: friend.mutableUsername, + exists = remember { context.modDatabase.getFriendInfo(friend.userId) != null } + ) { state -> + if (state) { + context.bridgeService.triggerFriendSync(cachedFriends!![it].userId) + } else { + context.modDatabase.deleteFriend(friend.userId) + } context.modDatabase.executeAsync { section.onResumed() } - }) + } } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt index 589ef844..55643c0b 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.core.messaging.MessagingRuleType @@ -36,25 +37,19 @@ class ScopeContent( private val context: RemoteSideContext, private val section: SocialSection, private val navController: NavController, - private val scope: SocialScope, + val scope: SocialScope, private val id: String ) { - @Composable - private fun DeleteScopeEntityButton() { - val coroutineScope = rememberCoroutineScope() - OutlinedButton(onClick = { - when (scope) { - SocialScope.FRIEND -> context.modDatabase.deleteFriend(id) - SocialScope.GROUP -> context.modDatabase.deleteGroup(id) + fun deleteScope(coroutineScope: CoroutineScope) { + when (scope) { + SocialScope.FRIEND -> context.modDatabase.deleteFriend(id) + SocialScope.GROUP -> context.modDatabase.deleteGroup(id) + } + context.modDatabase.executeAsync { + coroutineScope.launch { + section.onResumed() + navController.popBackStack() } - context.modDatabase.executeAsync { - coroutineScope.launch { - section.onResumed() - navController.navigate(SocialSection.MAIN_ROUTE) - } - } - }) { - Text(text = "Delete ${scope.key}") } } @@ -253,7 +248,6 @@ class ScopeContent( Text(text = group.name, maxLines = 1) Text(text = "participantsCount: ${group.participantsCount}", maxLines = 1) Spacer(modifier = Modifier.height(16.dp)) - DeleteScopeEntityButton() } } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt index fe2aba5b..6c59df29 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -15,9 +16,12 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.DeleteForever import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Tab @@ -32,8 +36,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.navigation @@ -42,6 +49,7 @@ import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo import me.rhunk.snapenhance.core.messaging.SocialScope import me.rhunk.snapenhance.ui.manager.Section +import me.rhunk.snapenhance.ui.util.AlertDialogs import me.rhunk.snapenhance.ui.util.BitmojiImage import me.rhunk.snapenhance.ui.util.pagerTabIndicatorOffset import me.rhunk.snapenhance.util.snap.BitmojiSelfie @@ -89,6 +97,35 @@ class SocialSection : Section() { } } + @Composable + override fun TopBarActions(rowScope: RowScope) { + var deleteConfirmDialog by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + + if (deleteConfirmDialog) { + currentScopeContent?.let { scopeContent -> + Dialog(onDismissRequest = { deleteConfirmDialog = false }) { + remember { AlertDialogs(context.translation) }.ConfirmDialog( + title = "Are you sure you want to delete this ${scopeContent.scope.key.lowercase()}?", + onDismiss = { deleteConfirmDialog = false }, + onConfirm = { scopeContent.deleteScope(coroutineScope); deleteConfirmDialog = false } + ) + } + } + } + + if (navController.currentBackStackEntry?.destination?.route != MAIN_ROUTE) { + IconButton( + onClick = { deleteConfirmDialog = true }, + ) { + Icon( + imageVector = Icons.Rounded.DeleteForever, + contentDescription = null + ) + } + } + } + @Composable private fun ScopeList(scope: SocialScope) { @@ -154,8 +191,8 @@ class SocialSection : Section() { .padding(10.dp) .fillMaxWidth() ) { - Text(text = friend.displayName ?: friend.mutableUsername, maxLines = 1) - Text(text = friend.userId, maxLines = 1) + Text(text = friend.displayName ?: friend.mutableUsername, maxLines = 1, fontWeight = FontWeight.Bold) + Text(text = friend.userId, maxLines = 1, fontSize = 12.sp) } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt index bf275033..ad37dfe2 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.ui.setup.screens.SetupScreen +import me.rhunk.snapenhance.ui.util.AlertDialogs class MappingsScreen : SetupScreen() { @Composable @@ -35,21 +36,8 @@ class MappingsScreen : SetupScreen() { Dialog(onDismissRequest = { infoText = null }) { - Surface( - modifier = Modifier.padding(16.dp).fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Text(text = infoText!!) - Button(onClick = { - infoText = null - }, - modifier = Modifier.padding(top = 5.dp).align(alignment = androidx.compose.ui.Alignment.End)) { - Text(text = "OK") - } - } + remember { AlertDialogs(context.translation) }.InfoDialog(title = infoText!!) { + infoText = null } } } @@ -87,7 +75,9 @@ class MappingsScreen : SetupScreen() { }) { if (isGenerating) { CircularProgressIndicator( - modifier = Modifier.padding().size(30.dp), + modifier = Modifier + .padding() + .size(30.dp), strokeWidth = 3.dp, color = MaterialTheme.colorScheme.onPrimary ) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt similarity index 79% rename from app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt index fef1731e..12667424 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt @@ -1,4 +1,4 @@ -package me.rhunk.snapenhance.ui.manager.sections.features +package me.rhunk.snapenhance.ui.util import androidx.compose.foundation.ScrollState import androidx.compose.foundation.clickable @@ -36,7 +36,7 @@ import me.rhunk.snapenhance.core.config.DataProcessors import me.rhunk.snapenhance.core.config.PropertyPair -class Dialogs( +class AlertDialogs( private val translation: LocaleWrapper, ){ @Composable @@ -54,6 +54,70 @@ class Dialogs( } } + @Composable + fun ConfirmDialog( + title: String, + data: String? = null, + onConfirm: () -> Unit, + onDismiss: () -> Unit, + ) { + DefaultDialogCard { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(bottom = 10.dp) + ) + if (data != null) { + Text( + text = data, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 10.dp) + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Button(onClick = { onDismiss() }) { + Text(text = translation["button.cancel"]) + } + Button(onClick = { onConfirm() }) { + Text(text = translation["button.ok"]) + } + } + } + } + + @Composable + fun InfoDialog( + title: String, + data: String? = null, + onDismiss: () -> Unit, + ) { + DefaultDialogCard { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(bottom = 10.dp) + ) + if (data != null) { + Text( + text = data, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 10.dp) + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Button(onClick = { onDismiss() }) { + Text(text = translation["button.ok"]) + } + } + } + } + @Composable fun TranslatedText(property: PropertyPair<*>, key: String, modifier: Modifier = Modifier) { Text(