commit 7703d3f007dc661624de1c2c6786d51132add223
parent 5fbfd5030ca83f3f2ad5a9fc31b55cc33dac06d5
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Wed, 23 Aug 2023 16:19:31 +0200

feat: better friend add list
- alert dialogs

Diffstat:
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt | 212-------------------------------------------------------------------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt | 9+++++----
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt | 206++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt | 28+++++++++++-----------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt | 41+++++++++++++++++++++++++++++++++++++++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt | 22++++++----------------
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt | 276+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 509 insertions(+), 285 deletions(-)

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/manager/sections/features/Dialogs.kt @@ -1,212 +0,0 @@ -package me.rhunk.snapenhance.ui.manager.sections.features - -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper -import me.rhunk.snapenhance.core.config.DataProcessors -import me.rhunk.snapenhance.core.config.PropertyPair - - -class Dialogs( - private val translation: LocaleWrapper, -){ - @Composable - fun DefaultDialogCard(content: @Composable ColumnScope.() -> Unit) { - Card( - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .padding(10.dp, 5.dp, 10.dp, 10.dp), - ) { - Column( - modifier = Modifier - .padding(10.dp, 10.dp, 10.dp, 10.dp) - .verticalScroll(ScrollState(0)), - ) { content() } - } - } - - @Composable - fun TranslatedText(property: PropertyPair<*>, key: String, modifier: Modifier = Modifier) { - Text( - text = property.key.propertyOption(translation, key), - modifier = Modifier - .padding(10.dp, 10.dp, 10.dp, 10.dp) - .then(modifier) - ) - } - - @Composable - @Suppress("UNCHECKED_CAST") - fun UniqueSelectionDialog(property: PropertyPair<*>) { - val keys = (property.value.defaultValues as List<String>).toMutableList().apply { - add(0, "null") - } - - var selectedValue by remember { - mutableStateOf(property.value.getNullable()?.toString() ?: "null") - } - - DefaultDialogCard { - keys.forEachIndexed { index, item -> - fun select() { - selectedValue = item - property.value.setAny(if (index == 0) { - null - } else { - item - }) - } - - Row( - modifier = Modifier.clickable { select() }, - verticalAlignment = Alignment.CenterVertically - ) { - TranslatedText( - property = property, - key = item, - modifier = Modifier.weight(1f) - ) - RadioButton( - selected = selectedValue == item, - onClick = { select() } - ) - } - } - } - } - - @Composable - fun KeyboardInputDialog(property: PropertyPair<*>, dismiss: () -> Unit = {}) { - val focusRequester = remember { FocusRequester() } - - DefaultDialogCard { - val fieldValue = remember { - mutableStateOf(property.value.get().toString().let { - TextFieldValue( - text = it, - selection = TextRange(it.length) - ) - }) - } - - TextField( - modifier = Modifier - .fillMaxWidth() - .padding(all = 10.dp) - .onGloballyPositioned { - focusRequester.requestFocus() - } - .focusRequester(focusRequester), - value = fieldValue.value, - onValueChange = { - fieldValue.value = it - }, - keyboardOptions = when (property.key.dataType.type) { - DataProcessors.Type.INTEGER -> KeyboardOptions(keyboardType = KeyboardType.Number) - DataProcessors.Type.FLOAT -> KeyboardOptions(keyboardType = KeyboardType.Decimal) - else -> KeyboardOptions(keyboardType = KeyboardType.Text) - }, - singleLine = true - ) - - Row( - modifier = Modifier.padding(top = 10.dp).fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - Button(onClick = { dismiss() }) { - Text(text = "Cancel") - } - Button(onClick = { - when (property.key.dataType.type) { - DataProcessors.Type.INTEGER -> { - runCatching { - property.value.setAny(fieldValue.value.text.toInt()) - }.onFailure { - property.value.setAny(0) - } - } - DataProcessors.Type.FLOAT -> { - runCatching { - property.value.setAny(fieldValue.value.text.toFloat()) - }.onFailure { - property.value.setAny(0f) - } - } - else -> property.value.setAny(fieldValue.value.text) - } - dismiss() - }) { - Text(text = "Ok") - } - } - } - } - - @Composable - @Suppress("UNCHECKED_CAST") - fun MultipleSelectionDialog(property: PropertyPair<*>) { - val defaultItems = property.value.defaultValues as List<String> - val toggledStates = property.value.get() as MutableList<String> - DefaultDialogCard { - defaultItems.forEach { key -> - var state by remember { mutableStateOf(toggledStates.contains(key)) } - - fun toggle(value: Boolean? = null) { - state = value ?: !state - if (state) { - toggledStates.add(key) - } else { - toggledStates.remove(key) - } - } - - Row( - modifier = Modifier.clickable { toggle() }, - verticalAlignment = Alignment.CenterVertically - ) { - TranslatedText( - property = property, - key = key, - modifier = Modifier - .weight(1f) - ) - Switch( - checked = state, - onCheckedChange = { - toggle(it) - } - ) - } - } - } - } -} 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 @@ -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 @@ -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<String>) { + 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 @@ -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) - } - context.modDatabase.executeAsync { - coroutineScope.launch { - section.onResumed() - navController.navigate(SocialSection.MAIN_ROUTE) - } + 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() } - }) { - 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 @@ -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 @@ -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/util/AlertDialogs.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt @@ -0,0 +1,276 @@ +package me.rhunk.snapenhance.ui.util + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper +import me.rhunk.snapenhance.core.config.DataProcessors +import me.rhunk.snapenhance.core.config.PropertyPair + + +class AlertDialogs( + private val translation: LocaleWrapper, +){ + @Composable + fun DefaultDialogCard(content: @Composable ColumnScope.() -> Unit) { + Card( + shape = MaterialTheme.shapes.medium, + modifier = Modifier + .padding(10.dp, 5.dp, 10.dp, 10.dp), + ) { + Column( + modifier = Modifier + .padding(10.dp, 10.dp, 10.dp, 10.dp) + .verticalScroll(ScrollState(0)), + ) { content() } + } + } + + @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( + text = property.key.propertyOption(translation, key), + modifier = Modifier + .padding(10.dp, 10.dp, 10.dp, 10.dp) + .then(modifier) + ) + } + + @Composable + @Suppress("UNCHECKED_CAST") + fun UniqueSelectionDialog(property: PropertyPair<*>) { + val keys = (property.value.defaultValues as List<String>).toMutableList().apply { + add(0, "null") + } + + var selectedValue by remember { + mutableStateOf(property.value.getNullable()?.toString() ?: "null") + } + + DefaultDialogCard { + keys.forEachIndexed { index, item -> + fun select() { + selectedValue = item + property.value.setAny(if (index == 0) { + null + } else { + item + }) + } + + Row( + modifier = Modifier.clickable { select() }, + verticalAlignment = Alignment.CenterVertically + ) { + TranslatedText( + property = property, + key = item, + modifier = Modifier.weight(1f) + ) + RadioButton( + selected = selectedValue == item, + onClick = { select() } + ) + } + } + } + } + + @Composable + fun KeyboardInputDialog(property: PropertyPair<*>, dismiss: () -> Unit = {}) { + val focusRequester = remember { FocusRequester() } + + DefaultDialogCard { + val fieldValue = remember { + mutableStateOf(property.value.get().toString().let { + TextFieldValue( + text = it, + selection = TextRange(it.length) + ) + }) + } + + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp) + .onGloballyPositioned { + focusRequester.requestFocus() + } + .focusRequester(focusRequester), + value = fieldValue.value, + onValueChange = { + fieldValue.value = it + }, + keyboardOptions = when (property.key.dataType.type) { + DataProcessors.Type.INTEGER -> KeyboardOptions(keyboardType = KeyboardType.Number) + DataProcessors.Type.FLOAT -> KeyboardOptions(keyboardType = KeyboardType.Decimal) + else -> KeyboardOptions(keyboardType = KeyboardType.Text) + }, + singleLine = true + ) + + Row( + modifier = Modifier.padding(top = 10.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Button(onClick = { dismiss() }) { + Text(text = "Cancel") + } + Button(onClick = { + when (property.key.dataType.type) { + DataProcessors.Type.INTEGER -> { + runCatching { + property.value.setAny(fieldValue.value.text.toInt()) + }.onFailure { + property.value.setAny(0) + } + } + DataProcessors.Type.FLOAT -> { + runCatching { + property.value.setAny(fieldValue.value.text.toFloat()) + }.onFailure { + property.value.setAny(0f) + } + } + else -> property.value.setAny(fieldValue.value.text) + } + dismiss() + }) { + Text(text = "Ok") + } + } + } + } + + @Composable + @Suppress("UNCHECKED_CAST") + fun MultipleSelectionDialog(property: PropertyPair<*>) { + val defaultItems = property.value.defaultValues as List<String> + val toggledStates = property.value.get() as MutableList<String> + DefaultDialogCard { + defaultItems.forEach { key -> + var state by remember { mutableStateOf(toggledStates.contains(key)) } + + fun toggle(value: Boolean? = null) { + state = value ?: !state + if (state) { + toggledStates.add(key) + } else { + toggledStates.remove(key) + } + } + + Row( + modifier = Modifier.clickable { toggle() }, + verticalAlignment = Alignment.CenterVertically + ) { + TranslatedText( + property = property, + key = key, + modifier = Modifier + .weight(1f) + ) + Switch( + checked = state, + onCheckedChange = { + toggle(it) + } + ) + } + } + } + } +}