commit
3386d45a04
7 changed files with 121 additions and 41 deletions
|
@ -3,7 +3,6 @@ package be.ugent.sel.studeez.common.composable
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
@ -22,7 +21,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import be.ugent.sel.studeez.common.ext.fieldModifier
|
||||
import be.ugent.sel.studeez.resources
|
||||
import kotlin.math.sin
|
||||
import be.ugent.sel.studeez.R.drawable as AppIcon
|
||||
import be.ugent.sel.studeez.R.string as AppText
|
||||
|
||||
|
@ -47,7 +45,7 @@ fun LabelledInputField(
|
|||
value: String,
|
||||
onNewValue: (String) -> Unit,
|
||||
@StringRes label: Int,
|
||||
singleLine: Boolean = false
|
||||
singleLine: Boolean = true
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
|
@ -119,7 +117,9 @@ fun LabeledErrorTextField(
|
|||
initialValue: String,
|
||||
@StringRes label: Int,
|
||||
singleLine: Boolean = false,
|
||||
errorText: Int,
|
||||
isValid: MutableState<Boolean> = remember { mutableStateOf(true) },
|
||||
isFirst: MutableState<Boolean> = remember { mutableStateOf(false) },
|
||||
@StringRes errorText: Int,
|
||||
keyboardType: KeyboardType,
|
||||
predicate: (String) -> Boolean,
|
||||
onNewCorrectValue: (String) -> Unit
|
||||
|
@ -128,31 +128,28 @@ fun LabeledErrorTextField(
|
|||
mutableStateOf(initialValue)
|
||||
}
|
||||
|
||||
var isValid by remember {
|
||||
mutableStateOf(predicate(value))
|
||||
}
|
||||
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
modifier = modifier.fieldModifier(),
|
||||
value = value,
|
||||
onValueChange = { newText ->
|
||||
isFirst.value = false
|
||||
value = newText
|
||||
isValid = predicate(value)
|
||||
if (isValid) {
|
||||
isValid.value = predicate(value)
|
||||
if (isValid.value) {
|
||||
onNewCorrectValue(newText)
|
||||
}
|
||||
},
|
||||
singleLine = singleLine,
|
||||
label = { Text(text = stringResource(id = label)) },
|
||||
isError = !isValid,
|
||||
isError = !isValid.value && !isFirst.value,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = keyboardType,
|
||||
imeAction = ImeAction.Done
|
||||
)
|
||||
)
|
||||
|
||||
if (!isValid) {
|
||||
if (!isValid.value && !isFirst.value) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
text = stringResource(id = errorText),
|
||||
|
|
|
@ -3,6 +3,8 @@ package be.ugent.sel.studeez.screens.timer_form
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import be.ugent.sel.studeez.common.composable.DeleteButton
|
||||
import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
|
||||
import be.ugent.sel.studeez.common.composable.FormComposable
|
||||
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
|
||||
import be.ugent.sel.studeez.R.string as AppText
|
||||
|
@ -12,8 +14,16 @@ fun TimerAddRoute(
|
|||
popUp: () -> Unit,
|
||||
viewModel: TimerFormViewModel
|
||||
) {
|
||||
TimerFormScreen(popUp = popUp, getTimerInfo = viewModel::getTimerInfo, AppText.add_timer) {
|
||||
viewModel.saveTimer(it, goBack = popUp)
|
||||
|
||||
|
||||
TimerFormScreen(
|
||||
popUp = popUp,
|
||||
getTimerInfo = viewModel::getTimerInfo,
|
||||
extraButton= { },
|
||||
AppText.add_timer
|
||||
) {
|
||||
viewModel.saveTimer(it, goBack = {popUp(); popUp()})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +32,20 @@ fun TimerEditRoute(
|
|||
popUp: () -> Unit,
|
||||
viewModel: TimerFormViewModel
|
||||
) {
|
||||
TimerFormScreen(popUp = popUp, getTimerInfo = viewModel::getTimerInfo, AppText.edit_timer) {
|
||||
|
||||
@Composable
|
||||
fun deleteButton() {
|
||||
DeleteButton(text = AppText.delete_timer) {
|
||||
viewModel.deleteTimer(viewModel.getTimerInfo(), popUp)
|
||||
}
|
||||
}
|
||||
|
||||
TimerFormScreen(
|
||||
popUp = popUp,
|
||||
getTimerInfo = viewModel::getTimerInfo,
|
||||
extraButton= { deleteButton() },
|
||||
AppText.edit_timer
|
||||
) {
|
||||
viewModel.editTimer(it, goBack = popUp)
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +54,7 @@ fun TimerEditRoute(
|
|||
fun TimerFormScreen(
|
||||
popUp: () -> Unit,
|
||||
getTimerInfo: () -> TimerInfo,
|
||||
extraButton: @Composable () -> Unit,
|
||||
@StringRes label: Int,
|
||||
onConfirmClick: (TimerInfo) -> Unit
|
||||
) {
|
||||
|
@ -40,6 +64,6 @@ fun TimerFormScreen(
|
|||
title = stringResource(id = label),
|
||||
popUp = popUp
|
||||
) {
|
||||
timerFormScreen(onConfirmClick)
|
||||
timerFormScreen(onConfirmClick, extraButton)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,11 @@ class TimerFormViewModel @Inject constructor(
|
|||
goBack()
|
||||
}
|
||||
|
||||
fun deleteTimer(timerInfo: TimerInfo, goBack: () -> Unit) {
|
||||
timerDAO.deleteTimer(timerInfo)
|
||||
goBack()
|
||||
}
|
||||
|
||||
fun saveTimer(timerInfo: TimerInfo, goBack: () -> Unit) {
|
||||
timerDAO.saveTimer(timerInfo)
|
||||
goBack()
|
||||
|
|
|
@ -3,49 +3,82 @@ package be.ugent.sel.studeez.screens.timer_form.form_screens
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import be.ugent.sel.studeez.R
|
||||
import be.ugent.sel.studeez.common.composable.BasicButton
|
||||
import be.ugent.sel.studeez.common.composable.LabelledInputField
|
||||
import be.ugent.sel.studeez.common.composable.LabeledErrorTextField
|
||||
import be.ugent.sel.studeez.common.ext.basicButton
|
||||
import be.ugent.sel.studeez.common.snackbar.SnackbarManager
|
||||
import be.ugent.sel.studeez.data.local.models.timer_info.TimerInfo
|
||||
import be.ugent.sel.studeez.R.string as AppText
|
||||
|
||||
abstract class AbstractTimerFormScreen(private val timerInfo: TimerInfo) {
|
||||
|
||||
protected val valids = mutableMapOf(
|
||||
"name" to mutableStateOf(textPredicate(timerInfo.name)),
|
||||
"description" to mutableStateOf(textPredicate(timerInfo.description))
|
||||
)
|
||||
|
||||
protected val firsts = mutableMapOf(
|
||||
"name" to mutableStateOf(true),
|
||||
"description" to mutableStateOf(true)
|
||||
)
|
||||
|
||||
|
||||
@Composable
|
||||
operator fun invoke(onSaveClick: (TimerInfo) -> Unit) {
|
||||
|
||||
var name by remember { mutableStateOf(timerInfo.name) }
|
||||
var description by remember { mutableStateOf(timerInfo.description) }
|
||||
|
||||
// This shall rerun whenever name and description change
|
||||
timerInfo.name = name
|
||||
timerInfo.description = description
|
||||
operator fun invoke(
|
||||
onSaveClick: (TimerInfo) -> Unit,
|
||||
extraButton: @Composable () -> Unit = {},
|
||||
) {
|
||||
|
||||
Column {
|
||||
|
||||
// Fields that every timer shares (ommited id)
|
||||
LabelledInputField(
|
||||
value = name,
|
||||
onNewValue = { name = it },
|
||||
label = R.string.name
|
||||
)
|
||||
LabeledErrorTextField(
|
||||
initialValue = timerInfo.name,
|
||||
label = R.string.name,
|
||||
errorText = AppText.name_error,
|
||||
isValid = valids.getValue("name"),
|
||||
isFirst = firsts.getValue("name"),
|
||||
keyboardType = KeyboardType.Text,
|
||||
predicate = { it.isNotBlank() }
|
||||
) { correctName ->
|
||||
timerInfo.name = correctName
|
||||
}
|
||||
|
||||
LabelledInputField(
|
||||
value = description,
|
||||
onNewValue = { description = it },
|
||||
label = AppText.description,
|
||||
singleLine = false
|
||||
)
|
||||
LabeledErrorTextField(
|
||||
initialValue = timerInfo.description,
|
||||
label = R.string.description,
|
||||
errorText = AppText.description_error,
|
||||
isValid = valids.getValue("description"),
|
||||
isFirst = firsts.getValue("description"),
|
||||
singleLine = false,
|
||||
keyboardType = KeyboardType.Text,
|
||||
predicate = { textPredicate(it) }
|
||||
) { correctName ->
|
||||
timerInfo.description = correctName
|
||||
}
|
||||
|
||||
ExtraFields()
|
||||
|
||||
BasicButton(R.string.save, Modifier.basicButton()) {
|
||||
onSaveClick(timerInfo)
|
||||
if (valids.all { it.component2().value }) { // All fields are valid
|
||||
onSaveClick(timerInfo)
|
||||
} else {
|
||||
firsts.map {
|
||||
it.component2().value = false
|
||||
} // dont mask error because its not been filled out yet
|
||||
SnackbarManager.showMessage(AppText.fill_out_error)
|
||||
}
|
||||
}
|
||||
extraButton()
|
||||
}
|
||||
}
|
||||
|
||||
private fun textPredicate(text: String): Boolean {
|
||||
return text.isNotBlank()
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun ExtraFields() {
|
||||
// By default no extra fields, unless overwritten by subclass.
|
||||
|
|
|
@ -15,6 +15,8 @@ class BreakTimerFormScreen(
|
|||
private val breakTimerInfo: PomodoroTimerInfo
|
||||
): AbstractTimerFormScreen(breakTimerInfo) {
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
override fun ExtraFields() {
|
||||
// If the user presses the OK button on the timepicker, the time in the button should change
|
||||
|
@ -26,12 +28,17 @@ class BreakTimerFormScreen(
|
|||
breakTimerInfo.breakTime = newTime
|
||||
}
|
||||
|
||||
valids["repeats"] = remember {mutableStateOf(true)}
|
||||
firsts["repeats"] = remember { mutableStateOf(true) }
|
||||
|
||||
LabeledErrorTextField(
|
||||
initialValue = breakTimerInfo.repeats.toString(),
|
||||
label = R.string.repeats,
|
||||
errorText = AppText.repeats_error,
|
||||
isValid = valids.getValue("repeats"),
|
||||
isFirst = firsts.getValue("repeats"),
|
||||
keyboardType = KeyboardType.Decimal,
|
||||
predicate = { it.matches(Regex("[1-9]+\\d*")) }
|
||||
predicate = { isNumber(it) }
|
||||
) { correctlyTypedInt ->
|
||||
breakTimerInfo.repeats = correctlyTypedInt.toInt()
|
||||
}
|
||||
|
@ -39,6 +46,10 @@ class BreakTimerFormScreen(
|
|||
}
|
||||
}
|
||||
|
||||
fun isNumber(text: String): Boolean {
|
||||
return text.matches(Regex("[1-9]+\\d*"))
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun BreakEditScreenPreview() {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package be.ugent.sel.studeez.screens.timer_form.timer_type_select
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate
|
||||
import be.ugent.sel.studeez.data.local.models.timer_info.*
|
||||
|
@ -37,7 +37,10 @@ fun TimerTypeSelectScreen(
|
|||
) {
|
||||
TimerType.values().forEach { timerType ->
|
||||
val default: TimerInfo = defaultTimerInfo.getValue(timerType)
|
||||
Button(onClick = { viewModel.onTimerTypeChosen(default, open) }) {
|
||||
Button(
|
||||
onClick = { viewModel.onTimerTypeChosen(default, open) },
|
||||
modifier = Modifier.fillMaxWidth().padding(5.dp)
|
||||
) {
|
||||
Text(text = timerType.name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,8 +70,15 @@
|
|||
|
||||
<!-- Timers -->
|
||||
<string name="timers">Timers</string>
|
||||
<string name="delete_timer">Delete Timer</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="add_timer">Add timer</string>
|
||||
|
||||
<string name="name_error">Name should not be blank</string>
|
||||
<string name="description_error">Description should not be blank</string>
|
||||
<string name="fill_out_error">Fill out all the fields correctly!</string>
|
||||
|
||||
|
||||
<string name="pick_time">Select time</string>
|
||||
<string name="state_focus">Focus!</string>
|
||||
<plurals name="state_focus_remaining">
|
||||
|
|
Reference in a new issue