Skip to content

Commit

Permalink
SpeziViews
Browse files Browse the repository at this point in the history
  • Loading branch information
pauljohanneskraft committed Nov 9, 2024
1 parent c4a543c commit ccf1c2f
Show file tree
Hide file tree
Showing 30 changed files with 933 additions and 22 deletions.
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

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt#L4 <detekt.NoUnusedImports>

Unused import
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt:4:1: warning: Unused import (detekt.NoUnusedImports)

Check warning on line 4 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt#L4 <detekt.UnusedImports>

The import 'androidx.compose.foundation.layout.fillMaxWidth' is unused.
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt:4:1: warning: The import 'androidx.compose.foundation.layout.fillMaxWidth' is unused. (detekt.UnusedImports)
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

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt#L6 <detekt.NoUnusedImports>

Unused import
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt:6:1: warning: Unused import (detekt.NoUnusedImports)

Check warning on line 6 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt#L6 <detekt.UnusedImports>

The import 'androidx.compose.foundation.lazy.staggeredgrid.LazyHorizontalStaggeredGrid' is unused.
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt:6:1: warning: The import 'androidx.compose.foundation.lazy.staggeredgrid.LazyHorizontalStaggeredGrid' is unused. (detekt.UnusedImports)
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

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt#L7 <detekt.NoUnusedImports>

Unused import
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt:7:1: warning: Unused import (detekt.NoUnusedImports)

Check warning on line 7 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt#L7 <detekt.UnusedImports>

The import 'androidx.compose.material3.Divider' is unused.
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameFieldRow.kt:7:1: warning: The import 'androidx.compose.material3.Divider' is unused. (detekt.UnusedImports)
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

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt#L3 <detekt.NoUnusedImports>

Unused import
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt:3:1: warning: Unused import (detekt.NoUnusedImports)

Check warning on line 3 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt#L3 <detekt.UnusedImports>

The import 'android.app.Person' is unused.
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/personalInfo/fields/NameTextField.kt:3:1: warning: The import 'android.app.Person' is unused. (detekt.UnusedImports)
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

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L3 <detekt.NoUnusedImports>

Unused import
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt:3:1: warning: Unused import (detekt.NoUnusedImports)

Check warning on line 3 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L3 <detekt.UnusedImports>

The import 'android.provider.Settings.Global' is unused.
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt:3:1: warning: The import 'android.provider.Settings.Global' is unused. (detekt.UnusedImports)
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

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L6 <detekt.NoUnusedImports>

Unused import
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt:6:1: warning: Unused import (detekt.NoUnusedImports)

Check warning on line 6 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L6 <detekt.UnusedImports>

The import 'kotlinx.coroutines.CoroutineScope' is unused.
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt:6:1: warning: The import 'kotlinx.coroutines.CoroutineScope' is unused. (detekt.UnusedImports)
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

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L10 <detekt.NoUnusedImports>

Unused import
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt:10:1: warning: Unused import (detekt.NoUnusedImports)

Check warning on line 10 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L10 <detekt.UnusedImports>

The import 'kotlinx.coroutines.async' is unused.
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt:10:1: warning: The import 'kotlinx.coroutines.async' is unused. (detekt.UnusedImports)
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

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L14 <detekt.NoUnusedImports>

Unused import
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt:14:1: warning: Unused import (detekt.NoUnusedImports)

Check warning on line 14 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt

View workflow job for this annotation

GitHub Actions / detekt

[detekt] core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt#L14 <detekt.UnusedImports>

The import 'kotlinx.coroutines.runBlocking' is unused.
Raw output
/github/workspace/core/design/src/main/kotlin/edu/stanford/spezi/core/design/views/validation/ValidationEngine.kt:14:1: warning: The import 'kotlinx.coroutines.runBlocking' is unused. (detekt.UnusedImports)
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
}
}
}
Loading

0 comments on commit ccf1c2f

Please sign in to comment.