From 7cdc8eaf765fcec66e777b238fb3b6049456bf70 Mon Sep 17 00:00:00 2001 From: FunkyMuse Date: Wed, 11 Oct 2023 19:22:57 +0200 Subject: [PATCH] feat: add custom interval picker dialog and fix text fields not respecting initial selections --- .../commons/dialogs/AddBlockedNumberDialog.kt | 14 +- .../dialogs/CustomIntervalPickerDialog.kt | 184 +++++++++++++++++- .../commons/dialogs/FileConflictDialog.kt | 14 +- .../samples/activities/MainActivity.kt | 8 +- .../samples/activities/TestDialogActivity.kt | 18 +- 5 files changed, 214 insertions(+), 24 deletions(-) diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/AddBlockedNumberDialog.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/AddBlockedNumberDialog.kt index aa1fe2f74..89ecf991d 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/AddBlockedNumberDialog.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/AddBlockedNumberDialog.kt @@ -11,7 +11,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue import com.simplemobiletools.commons.R import com.simplemobiletools.commons.compose.alert_dialog.* import com.simplemobiletools.commons.compose.extensions.MyDevices @@ -27,7 +29,14 @@ fun AddOrEditBlockedNumberAlertDialog( addBlockedNumber: (String) -> Unit ) { val focusRequester = remember { FocusRequester() } - var textFieldValue by remember { mutableStateOf(blockedNumber?.number.orEmpty()) } + var textFieldValue by remember { + mutableStateOf( + TextFieldValue( + text = blockedNumber?.number.orEmpty(), + selection = TextRange(blockedNumber?.number?.length ?: 0) + ) + ) + } AlertDialog( containerColor = dialogContainerColor, @@ -36,11 +45,10 @@ fun AddOrEditBlockedNumberAlertDialog( onDismissRequest = alertDialogState::hide, confirmButton = { TextButton(onClick = { - var newBlockedNumber = textFieldValue + var newBlockedNumber = textFieldValue.text if (blockedNumber != null && newBlockedNumber != blockedNumber.number) { deleteBlockedNumber(blockedNumber.number) } - if (newBlockedNumber.isNotEmpty()) { // in case the user also added a '.' in the pattern, remove it if (newBlockedNumber.contains(".*")) { diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/CustomIntervalPickerDialog.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/CustomIntervalPickerDialog.kt index a8d6b6b24..0d3383688 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/CustomIntervalPickerDialog.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/CustomIntervalPickerDialog.kt @@ -1,22 +1,46 @@ package com.simplemobiletools.commons.dialogs import android.app.Activity +import android.content.Context import android.content.DialogInterface import android.view.KeyEvent import android.view.View import androidx.appcompat.app.AlertDialog -import androidx.compose.runtime.Composable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.* +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +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 androidx.compose.ui.window.DialogProperties import com.simplemobiletools.commons.R import com.simplemobiletools.commons.compose.alert_dialog.AlertDialogState +import com.simplemobiletools.commons.compose.alert_dialog.DialogSurface +import com.simplemobiletools.commons.compose.alert_dialog.ShowKeyboardWhenDialogIsOpenedAndRequestFocus import com.simplemobiletools.commons.compose.alert_dialog.rememberAlertDialogState +import com.simplemobiletools.commons.compose.components.RadioGroupDialogComponent import com.simplemobiletools.commons.compose.extensions.MyDevices import com.simplemobiletools.commons.compose.theme.AppThemeSurface +import com.simplemobiletools.commons.compose.theme.SimpleTheme import com.simplemobiletools.commons.databinding.DialogCustomIntervalPickerBinding import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.DAY_SECONDS import com.simplemobiletools.commons.helpers.HOUR_SECONDS import com.simplemobiletools.commons.helpers.MINUTE_SECONDS +import kotlinx.collections.immutable.toImmutableList class CustomIntervalPickerDialog(val activity: Activity, val selectedSeconds: Int = 0, val showSeconds: Boolean = false, val callback: (minutes: Int) -> Unit) { private var dialog: AlertDialog? = null @@ -91,16 +115,168 @@ class CustomIntervalPickerDialog(val activity: Activity, val selectedSeconds: In @Composable fun CustomIntervalPickerAlertDialog( alertDialogState: AlertDialogState, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + selectedSeconds: Int = 0, + showSeconds: Boolean = false, + callback: (minutes: Int) -> Unit ) { - //todo in progress + val focusRequester = remember { FocusRequester() } + var textFieldValue by remember { + mutableStateOf(initialTextFieldValue(selectedSeconds)) + } + + val context = LocalContext.current + val selections = remember { + buildCustomIntervalEntries(context, showSeconds) + } + val initiallySelected = remember { + initialSelection(selectedSeconds, context) + } + + val (selected, setSelected) = remember { mutableStateOf(initiallySelected) } + + AlertDialog( + modifier = modifier.fillMaxWidth(0.95f), + onDismissRequest = alertDialogState::hide, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + DialogSurface { + Box { + Column( + modifier = modifier + .padding(bottom = 64.dp) + .verticalScroll(rememberScrollState()) + ) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding( + top = SimpleTheme.dimens.padding.extraLarge, + start = SimpleTheme.dimens.padding.extraLarge, + end = SimpleTheme.dimens.padding.extraLarge + ) + .focusRequester(focusRequester), + value = textFieldValue, + onValueChange = { newValue -> + if (newValue.text.length <= 5) textFieldValue = newValue + }, + label = { + Text(text = stringResource(id = R.string.value)) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + maxLines = 1 + ) + + RadioGroupDialogComponent( + items = selections, + selected = selected, + setSelected = setSelected, + modifier = Modifier.padding( + vertical = SimpleTheme.dimens.padding.extraLarge, + ) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding( + top = SimpleTheme.dimens.padding.extraLarge, + bottom = SimpleTheme.dimens.padding.extraLarge, + end = SimpleTheme.dimens.padding.extraLarge + ) + .align(Alignment.BottomStart) + ) { + TextButton(onClick = alertDialogState::hide) { + Text(text = stringResource(id = R.string.cancel)) + } + + TextButton(onClick = { + val multiplier = getMultiplier(context, selected) + val minutes = Integer.valueOf(textFieldValue.text.ifEmpty { "0" }) + callback(minutes * multiplier) + alertDialogState.hide() + }) { + Text(text = stringResource(id = R.string.ok)) + } + } + } + } + } + ShowKeyboardWhenDialogIsOpenedAndRequestFocus(focusRequester = focusRequester) +} + +private fun initialSelection(selectedSeconds: Int, context: Context) = requireNotNull( + when { + selectedSeconds == 0 -> minutesRaw(context) + selectedSeconds % DAY_SECONDS == 0 -> daysRaw(context) + selectedSeconds % HOUR_SECONDS == 0 -> hoursRaw(context) + selectedSeconds % MINUTE_SECONDS == 0 -> minutesRaw(context) + else -> secondsRaw(context) + + } +) { + "Incorrect format, please check selections" +} + +private fun initialTextFieldValue(selectedSeconds: Int) = when { + selectedSeconds == 0 -> TextFieldValue("") + selectedSeconds % DAY_SECONDS == 0 -> { + val text = (selectedSeconds / DAY_SECONDS).toString() + textFieldValueAndSelection(text) + } + + selectedSeconds % HOUR_SECONDS == 0 -> { + val text = (selectedSeconds / HOUR_SECONDS).toString() + textFieldValueAndSelection(text) + } + + selectedSeconds % MINUTE_SECONDS == 0 -> { + val text = (selectedSeconds / MINUTE_SECONDS).toString() + textFieldValueAndSelection(text) + } + + else -> { + val text = selectedSeconds.toString() + textFieldValueAndSelection(text) + } +} + +private fun textFieldValueAndSelection(text: String) = TextFieldValue(text = text, selection = TextRange(text.length)) + +fun buildCustomIntervalEntries(context: Context, showSeconds: Boolean) = + buildList { + if (showSeconds) { + add(secondsRaw(context)) + } + add(minutesRaw(context)) + add(hoursRaw(context)) + add(daysRaw(context)) + }.toImmutableList() + +private fun daysRaw(context: Context) = context.getString(R.string.days_raw) +private fun hoursRaw(context: Context) = context.getString(R.string.hours_raw) +private fun secondsRaw(context: Context) = context.getString(R.string.seconds_raw) +private fun minutesRaw(context: Context) = context.getString(R.string.minutes_raw) + +private fun getMultiplier(context: Context, text: String) = when (text) { + daysRaw(context) -> DAY_SECONDS + hoursRaw(context) -> HOUR_SECONDS + minutesRaw(context) -> MINUTE_SECONDS + else -> 1 } @Composable @MyDevices private fun CustomIntervalPickerAlertDialogPreview() { AppThemeSurface { - CustomIntervalPickerAlertDialog(alertDialogState = rememberAlertDialogState()) + CustomIntervalPickerAlertDialog(alertDialogState = rememberAlertDialogState(), + selectedSeconds = 0, + showSeconds = true, + callback = {} + ) } } diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/FileConflictDialog.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/FileConflictDialog.kt index da45d9d57..de7019db9 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/FileConflictDialog.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/FileConflictDialog.kt @@ -2,7 +2,6 @@ package com.simplemobiletools.commons.dialogs import android.app.Activity import android.content.Context -import androidx.annotation.StringRes import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -102,7 +101,7 @@ fun FileConflictAlertDialog( val context = LocalContext.current var isShowApplyForAllChecked by remember { mutableStateOf(context.baseConfig.lastConflictApplyToAll) } val selections = remember { - buildEntries(context, fileDirItem.isDirectory) + buildFileConflictEntries(context, fileDirItem.isDirectory) } val kinds = remember { selections.values.toImmutableList() @@ -190,17 +189,16 @@ fun FileConflictAlertDialog( } } -private fun buildEntries(context: Context, directory: Boolean) = +private fun buildFileConflictEntries(context: Context, directory: Boolean) = buildMap { - this[CONFLICT_SKIP] = getKind(R.string.skip, context) + this[CONFLICT_SKIP] = context.getString(R.string.skip) if (directory) { - this[CONFLICT_SKIP] = getKind(R.string.merge, context) + this[CONFLICT_SKIP] = context.getString(R.string.merge) } - this[CONFLICT_OVERWRITE] = getKind(R.string.overwrite, context) - this[CONFLICT_KEEP_BOTH] = getKind(R.string.keep_both, context) + this[CONFLICT_OVERWRITE] = context.getString(R.string.overwrite) + this[CONFLICT_KEEP_BOTH] = context.getString(R.string.keep_both) } -private fun getKind(@StringRes resId: Int, context: Context): String = context.getString(resId) @MyDevices @Composable diff --git a/samples/src/main/kotlin/com/simplemobiletools/commons/samples/activities/MainActivity.kt b/samples/src/main/kotlin/com/simplemobiletools/commons/samples/activities/MainActivity.kt index cef57348f..9056b7347 100644 --- a/samples/src/main/kotlin/com/simplemobiletools/commons/samples/activities/MainActivity.kt +++ b/samples/src/main/kotlin/com/simplemobiletools/commons/samples/activities/MainActivity.kt @@ -2,16 +2,16 @@ package com.simplemobiletools.commons.samples.activities import android.content.Intent import android.os.Bundle +import android.util.Log import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.activities.ManageBlockedNumbersActivity import com.simplemobiletools.commons.dialogs.BottomSheetChooserDialog -import com.simplemobiletools.commons.dialogs.FileConflictDialog +import com.simplemobiletools.commons.dialogs.CustomIntervalPickerDialog import com.simplemobiletools.commons.extensions.appLaunched import com.simplemobiletools.commons.extensions.toast import com.simplemobiletools.commons.extensions.viewBinding import com.simplemobiletools.commons.helpers.LICENSE_AUTOFITTEXTVIEW import com.simplemobiletools.commons.models.FAQItem -import com.simplemobiletools.commons.models.FileDirItem import com.simplemobiletools.commons.models.SimpleListItem import com.simplemobiletools.commons.samples.BuildConfig import com.simplemobiletools.commons.samples.R @@ -49,8 +49,8 @@ class MainActivity : BaseSimpleActivity() { startActivity(Intent(this, TestDialogActivity::class.java)) } binding.testButton.setOnClickListener { - FileConflictDialog(this, FileDirItem(filesDir.absolutePath, name = "Test"), false) { resolution, applyForAll -> - + CustomIntervalPickerDialog(this, 0, showSeconds = false) { + Log.d("CustomIntervalPickerDialog", it.toString()) } } } diff --git a/samples/src/main/kotlin/com/simplemobiletools/commons/samples/activities/TestDialogActivity.kt b/samples/src/main/kotlin/com/simplemobiletools/commons/samples/activities/TestDialogActivity.kt index a81cb9c09..a63782abd 100644 --- a/samples/src/main/kotlin/com/simplemobiletools/commons/samples/activities/TestDialogActivity.kt +++ b/samples/src/main/kotlin/com/simplemobiletools/commons/samples/activities/TestDialogActivity.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf @@ -39,7 +38,6 @@ class TestDialogActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { AppThemeSurface { - MaterialTheme Column( Modifier .fillMaxSize() @@ -48,7 +46,7 @@ class TestDialogActivity : ComponentActivity() { verticalArrangement = Arrangement.spacedBy(16.dp) ) { Spacer(modifier = Modifier.padding(top = 16.dp)) - ShowButton(getAppSideLoadedDialogState(), text = "App side loaded dialog") + ShowButton(getAppSideLoadedDialogState(), text = "App side loaded") ShowButton(getAddBlockedNumberDialogState(), text = "Add blocked number") ShowButton(getConfirmationAlertDialogState(), text = "Confirmation normal") ShowButton(getConfirmationAdvancedAlertDialogState(), text = "Confirmation advanced") @@ -65,18 +63,28 @@ class TestDialogActivity : ComponentActivity() { ShowButton(getUpgradeToProAlertDialogState(), text = "Upgrade to pro") ShowButton(getWhatsNewAlertDialogState(), text = "What's new") ShowButton(getChangeViewTypeAlertDialogState(), text = "Change view type") - ShowButton(getWritePermissionAlertDialogState(), text = "Write permission dialog") + ShowButton(getWritePermissionAlertDialogState(), text = "Write permission") ShowButton(getCreateNewFolderAlertDialogState(), text = "Create new folder") ShowButton(getEnterPasswordAlertDialogState(), text = "Enter password") ShowButton(getFolderLockingNoticeAlertDialogState(), text = "Folder locking notice") ShowButton(getChooserBottomSheetDialogState(), text = "Bottom sheet chooser") - ShowButton(getFileConflictAlertDialogState(), text = "File conflict dialog") + ShowButton(getFileConflictAlertDialogState(), text = "File conflict") + ShowButton(getCustomIntervalPickerAlertDialogState(), text = "Custom interval picker") Spacer(modifier = Modifier.padding(bottom = 16.dp)) } } } } + @Composable + private fun getCustomIntervalPickerAlertDialogState() = rememberAlertDialogState().apply { + DialogMember { + CustomIntervalPickerAlertDialog(alertDialogState = this, selectedSeconds = 3, showSeconds = true) { + Log.d("CustomIntervalPickerAlertDialog", it.toString()) + } + } + } + @Composable private fun getFileConflictAlertDialogState() = rememberAlertDialogState().apply { DialogMember {