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:
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)
+ }
+ )
+ }
+ }
+ }
+ }
+}