-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package edu.stanford.spezi.core.design.views.personalInfo | ||
|
||
data class PersonNameComponents( | ||
var namePrefix: String? = null, | ||
var givenName: String? = null, | ||
var middleName: String? = null, | ||
var familyName: String? = null, | ||
var nameSuffix: String? = null, | ||
var nickname: String? = null, | ||
) { | ||
enum class FormatStyle { | ||
ABBREVIATED, SHORT, MEDIUM, LONG | ||
} | ||
|
||
fun formatted(style: FormatStyle = FormatStyle.LONG): String { | ||
return when (style) { | ||
FormatStyle.LONG -> listOfNotNull( | ||
namePrefix, | ||
givenName, | ||
nickname?.let { "\"$it\"" }, | ||
middleName, | ||
familyName, | ||
nameSuffix | ||
).joinToString(" ") | ||
FormatStyle.MEDIUM -> | ||
TODO("Not yet implemented.") | ||
FormatStyle.SHORT -> | ||
TODO("Not yet implemented.") | ||
FormatStyle.ABBREVIATED -> listOfNotNull( | ||
givenName, | ||
middleName, | ||
familyName, | ||
).joinToString("") | ||
.filter { it.isUpperCase() } | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package edu.stanford.spezi.core.design.views.personalInfo | ||
|
||
import androidx.compose.foundation.Image | ||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.aspectRatio | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.size | ||
import androidx.compose.foundation.shape.CircleShape | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.LaunchedEffect | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.clip | ||
import androidx.compose.ui.graphics.vector.ImageVector | ||
import androidx.compose.ui.layout.onSizeChanged | ||
import androidx.compose.ui.unit.IntSize | ||
import androidx.compose.ui.unit.dp | ||
import androidx.compose.ui.unit.sp | ||
import edu.stanford.spezi.core.design.theme.Colors | ||
import edu.stanford.spezi.core.design.theme.lighten | ||
import kotlin.math.min | ||
|
||
@Composable | ||
fun UserProfileComposable( | ||
modifier: Modifier = Modifier, | ||
name: PersonNameComponents, | ||
imageLoader: suspend () -> ImageVector? = { null }, // TODO: Use ImageResource instead! | ||
) { | ||
val image = remember { mutableStateOf<ImageVector?>(null) } | ||
val size = remember { mutableStateOf(IntSize.Zero) } | ||
|
||
LaunchedEffect(Unit) { | ||
image.value = imageLoader() | ||
} | ||
|
||
Box(modifier.onSizeChanged { size.value = it }.aspectRatio(1f)) { | ||
val sideLength = min(size.value.height, size.value.width).dp | ||
Box(modifier.size(sideLength, sideLength), contentAlignment = Alignment.Center) { | ||
image.value?.let { | ||
Image( | ||
it, | ||
null, | ||
Modifier | ||
.clip(CircleShape) | ||
.background(Colors.background, CircleShape) | ||
) | ||
} ?: run { | ||
Box(Modifier.background(Colors.secondary, CircleShape).fillMaxSize(), contentAlignment = Alignment.Center) { | ||
Text( | ||
name.formatted(PersonNameComponents.FormatStyle.ABBREVIATED), | ||
fontSize = (sideLength.value * 0.2).sp, | ||
color = Colors.secondary.lighten(), | ||
) | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package edu.stanford.spezi.core.design.views.personalInfo.fields | ||
|
||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.fillMaxWidth | ||
Check warning on line 4 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt#L4 <detekt.NoUnusedImports>
Raw output
Check warning on line 4 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt#L4 <detekt.UnusedImports>
Raw output
|
||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.foundation.lazy.staggeredgrid.LazyHorizontalStaggeredGrid | ||
Check warning on line 6 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt#L6 <detekt.NoUnusedImports>
Raw output
Check warning on line 6 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt#L6 <detekt.UnusedImports>
Raw output
|
||
import androidx.compose.material3.Divider | ||
Check warning on line 7 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt#L7 <detekt.NoUnusedImports>
Raw output
Check warning on line 7 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt#L7 <detekt.UnusedImports>
Raw output
|
||
import androidx.compose.material3.HorizontalDivider | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.MutableState | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.unit.dp | ||
import edu.stanford.spezi.core.design.component.StringResource | ||
import edu.stanford.spezi.core.design.theme.ThemePreviews | ||
import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents | ||
import edu.stanford.spezi.core.design.views.views.layout.DescriptionGridRow | ||
import kotlin.reflect.KMutableProperty1 | ||
|
||
@Composable | ||
fun NameFieldRow( | ||
description: StringResource, | ||
name: MutableState<PersonNameComponents>, | ||
component: KMutableProperty1<PersonNameComponents, String?>, | ||
label: @Composable () -> Unit, | ||
) { | ||
NameFieldRow( | ||
name = name, | ||
component = component, | ||
description = { Text(description.text()) }, | ||
label = label | ||
) | ||
} | ||
|
||
@Composable | ||
fun NameFieldRow( | ||
name: MutableState<PersonNameComponents>, | ||
component: KMutableProperty1<PersonNameComponents, String?>, | ||
description: @Composable () -> Unit, | ||
label: @Composable () -> Unit, | ||
) { | ||
DescriptionGridRow( | ||
description = description, | ||
content = { | ||
NameTextField(name, component) { | ||
label() | ||
} | ||
} | ||
) | ||
} | ||
|
||
@ThemePreviews | ||
@Composable | ||
private fun NameFieldRowPreview() { | ||
val name = remember { mutableStateOf(PersonNameComponents()) } | ||
|
||
Column { | ||
NameFieldRow( | ||
name, | ||
PersonNameComponents::givenName, | ||
description = { Text("First") } | ||
) { | ||
Text("enter first name") | ||
} | ||
|
||
HorizontalDivider(Modifier.padding(vertical = 15.dp)) | ||
|
||
// Last Name Field | ||
NameFieldRow( | ||
name, | ||
PersonNameComponents::familyName, | ||
description = { Text("Last") } | ||
) { | ||
Text("enter last name") | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package edu.stanford.spezi.core.design.views.personalInfo.fields | ||
|
||
import android.app.Person | ||
Check warning on line 3 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt#L3 <detekt.NoUnusedImports>
Raw output
Check warning on line 3 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt#L3 <detekt.UnusedImports>
Raw output
|
||
import androidx.compose.foundation.text.KeyboardOptions | ||
import androidx.compose.material3.Text | ||
import androidx.compose.material3.TextField | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.MutableState | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import edu.stanford.spezi.core.design.component.StringResource | ||
import edu.stanford.spezi.core.design.theme.ThemePreviews | ||
import edu.stanford.spezi.core.design.views.personalInfo.PersonNameComponents | ||
import kotlin.reflect.KMutableProperty1 | ||
|
||
@Composable | ||
fun NameTextField( | ||
label: StringResource, | ||
name: MutableState<PersonNameComponents>, | ||
component: KMutableProperty1<PersonNameComponents, String?>, | ||
prompt: StringResource? = null, | ||
) { | ||
NameTextField(name, component, prompt) { | ||
Text(label.text()) | ||
} | ||
} | ||
|
||
@Composable | ||
fun NameTextField( | ||
name: MutableState<PersonNameComponents>, | ||
component: KMutableProperty1<PersonNameComponents, String?>, | ||
prompt: StringResource? = null, | ||
label: @Composable () -> Unit, | ||
) { | ||
// TODO: Figure out which other options to set on the keyboard for names | ||
TextField( | ||
component.get(name.value) ?: "", | ||
onValueChange = { | ||
if (it.isBlank()) { | ||
component.set(name.value, null) | ||
} else { | ||
component.set(name.value, it) | ||
} | ||
}, | ||
keyboardOptions = KeyboardOptions( | ||
autoCorrect = false, | ||
), | ||
// TODO: Check if placeholder is the right fit for the prompt property here. | ||
placeholder = prompt?.let { { Text(it.text()) } }, | ||
label = label | ||
) | ||
} | ||
|
||
@ThemePreviews | ||
@Composable | ||
private fun NameTextFieldPreview() { | ||
val name = remember { mutableStateOf(PersonNameComponents()) } | ||
|
||
NameTextField(name, PersonNameComponents::givenName) { | ||
Text("Enter first name") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package edu.stanford.spezi.core.design.views.validation | ||
|
||
enum class CascadingValidationEffect { | ||
CONTINUE, INTERCEPT | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package edu.stanford.spezi.core.design.views.validation | ||
|
||
import android.provider.Settings.Global | ||
Check warning on line 3 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L3 <detekt.NoUnusedImports>
Raw output
Check warning on line 3 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L3 <detekt.UnusedImports>
Raw output
|
||
import edu.stanford.spezi.core.design.views.validation.configuration.DEFAULT_VALIDATION_DEBOUNCE_DURATION | ||
import edu.stanford.spezi.core.design.views.validation.state.FailedValidationResult | ||
import kotlinx.coroutines.CoroutineScope | ||
Check warning on line 6 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L6 <detekt.NoUnusedImports>
Raw output
Check warning on line 6 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L6 <detekt.UnusedImports>
Raw output
|
||
import kotlinx.coroutines.DelicateCoroutinesApi | ||
import kotlinx.coroutines.GlobalScope | ||
import kotlinx.coroutines.Job | ||
import kotlinx.coroutines.async | ||
Check warning on line 10 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L10 <detekt.NoUnusedImports>
Raw output
Check warning on line 10 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L10 <detekt.UnusedImports>
Raw output
|
||
import kotlinx.coroutines.delay | ||
import kotlinx.coroutines.isActive | ||
import kotlinx.coroutines.launch | ||
import kotlinx.coroutines.runBlocking | ||
Check warning on line 14 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L14 <detekt.NoUnusedImports>
Raw output
Check warning on line 14 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt GitHub Actions / detekt[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L14 <detekt.UnusedImports>
Raw output
|
||
import java.util.EnumSet | ||
import kotlin.time.Duration | ||
|
||
typealias ValidationEngineConfiguration = EnumSet<ValidationEngine.ConfigurationOption> | ||
|
||
class ValidationEngine( | ||
val rules: List<ValidationRule>, | ||
var debounceDuration: Duration = DEFAULT_VALIDATION_DEBOUNCE_DURATION, | ||
var configuration: ValidationEngineConfiguration = ValidationEngineConfiguration.noneOf(ConfigurationOption::class.java), | ||
) { | ||
private enum class Source { | ||
SUBMIT, MANUAL | ||
} | ||
|
||
enum class ConfigurationOption { | ||
HIDE_FAILED_VALIDATION_ON_EMPTY_SUBMIT, | ||
CONSIDER_NO_INPUT_AS_VALID, | ||
} | ||
|
||
var validationResults: List<FailedValidationResult> = emptyList() | ||
private set | ||
|
||
private var computedInputValid: Boolean? = null | ||
|
||
val inputValid: Boolean get() = | ||
computedInputValid ?: configuration.contains(ConfigurationOption.CONSIDER_NO_INPUT_AS_VALID) | ||
|
||
private var source: Source? = null | ||
private var inputWasEmpty = true | ||
|
||
val isDisplayingValidationErrors: Boolean get() { | ||
val gotResults = validationResults.isNotEmpty() | ||
|
||
if (configuration.contains(ConfigurationOption.HIDE_FAILED_VALIDATION_ON_EMPTY_SUBMIT)) { | ||
return gotResults && (source == Source.MANUAL || !inputWasEmpty) | ||
} | ||
|
||
return gotResults | ||
} | ||
|
||
val displayedValidationResults: List<FailedValidationResult> get() = | ||
if (isDisplayingValidationErrors) validationResults else emptyList() | ||
|
||
private var debounceJob: Job? = null | ||
|
||
@Suppress("detekt:LoopWithTooManyJumpStatements") | ||
private fun computeFailedValidations(input: String): List<FailedValidationResult> { | ||
val results = mutableListOf<FailedValidationResult>() | ||
|
||
for (rule in rules) { | ||
val result = rule.validate(input) ?: break | ||
results.add(result) | ||
// TODO: Logging | ||
if (rule.effect == CascadingValidationEffect.INTERCEPT) break | ||
} | ||
|
||
return results | ||
} | ||
|
||
private fun computeValidation(input: String, source: Source) { | ||
this.source = source | ||
this.inputWasEmpty = input.isEmpty() | ||
|
||
this.validationResults = computeFailedValidations(input) | ||
this.computedInputValid = validationResults.isEmpty() | ||
} | ||
|
||
fun submit(input: String, debounce: Boolean = false) { | ||
if (!debounce || computedInputValid == false) { | ||
computeValidation(input, Source.SUBMIT) | ||
} else { | ||
this.debounce { | ||
this.computeValidation(input, Source.SUBMIT) | ||
} | ||
} | ||
} | ||
|
||
fun runValidation(input: String) { | ||
computeValidation(input, Source.MANUAL) | ||
} | ||
|
||
@OptIn(DelicateCoroutinesApi::class) | ||
private fun debounce(task: () -> Unit) { | ||
debounceJob?.cancel() | ||
// TODO: Think about whether to not use GlobalScope here | ||
debounceJob = GlobalScope.launch { | ||
delay(debounceDuration) | ||
|
||
if (!isActive) return@launch | ||
|
||
task() | ||
debounceJob = null | ||
} | ||
} | ||
} |