Repo created
This commit is contained in:
parent
75dc487a7a
commit
39c29d175b
6317 changed files with 388324 additions and 2 deletions
7
feature/account/avatar/api/build.gradle.kts
Normal file
7
feature/account/avatar/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.kmp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.thunderbird.feature.account.avatar"
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package net.thunderbird.feature.account.avatar
|
||||
|
||||
/**
|
||||
* Interface for creating a monogram based on a name or email address.
|
||||
*
|
||||
* This interface is used to generate a monogram, which is typically the initials of a person's name,
|
||||
* or a representation based on an email address. Implementations should handle null or empty inputs gracefully.
|
||||
*/
|
||||
fun interface AvatarMonogramCreator {
|
||||
/**
|
||||
* Creates a monogram for the given name or email.
|
||||
*
|
||||
* @param name The name to generate a monogram for.
|
||||
* @param email The email address to generate a monogram for.
|
||||
* @return A string representing the monogram, or an empty string if the name or email is null or empty.
|
||||
*/
|
||||
fun create(name: String?, email: String?): String
|
||||
}
|
||||
17
feature/account/avatar/impl/build.gradle.kts
Normal file
17
feature/account/avatar/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.androidCompose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.thunderbird.feature.account.avatar.impl"
|
||||
resourcePrefix = "account_avatar_"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.ui.compose.designsystem)
|
||||
implementation(projects.core.common)
|
||||
|
||||
implementation(projects.feature.account.avatar.api)
|
||||
|
||||
testImplementation(projects.core.ui.compose.testing)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package net.thunderbird.feature.account.avatar.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AvatarOutlinedPreview() {
|
||||
PreviewWithThemes {
|
||||
AvatarOutlined(
|
||||
color = Color(0xFFe57373),
|
||||
name = "example",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AvatarOutlinedLargePreview() {
|
||||
PreviewWithThemes {
|
||||
AvatarOutlined(
|
||||
color = Color(0xFFe57373),
|
||||
name = "example",
|
||||
size = AvatarSize.LARGE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package net.thunderbird.feature.account.avatar.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AvatarPreview() {
|
||||
PreviewWithThemes {
|
||||
Avatar(
|
||||
color = Color(0xFFe57373),
|
||||
name = "example",
|
||||
selected = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AvatarSelectedPreview() {
|
||||
PreviewWithThemes {
|
||||
Avatar(
|
||||
color = Color(0xFFe57373),
|
||||
name = "example",
|
||||
selected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package net.thunderbird.feature.account.avatar
|
||||
|
||||
/**
|
||||
* Creates a monogram based on a name or email address.
|
||||
*
|
||||
* This implementation generates a monogram by taking the first two characters of the name or email,
|
||||
* removing spaces, and converting them to uppercase.
|
||||
*/
|
||||
class DefaultAvatarMonogramCreator : AvatarMonogramCreator {
|
||||
override fun create(name: String?, email: String?): String {
|
||||
return if (name != null && name.isNotEmpty()) {
|
||||
composeAvatarMonogram(name)
|
||||
} else if (email != null && email.isNotEmpty()) {
|
||||
composeAvatarMonogram(email)
|
||||
} else {
|
||||
AVATAR_MONOGRAM_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
private fun composeAvatarMonogram(name: String): String {
|
||||
return name.replace(" ", "").take(2).uppercase()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val AVATAR_MONOGRAM_DEFAULT = "XX"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package net.thunderbird.feature.account.avatar.ui
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.Surface
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
|
||||
val selectedAvatarSize = 40.dp
|
||||
|
||||
@Composable
|
||||
fun Avatar(
|
||||
color: Color,
|
||||
name: String,
|
||||
selected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val avatarSize by animateDpAsState(
|
||||
targetValue = if (selected) selectedAvatarSize else MainTheme.sizes.iconAvatar,
|
||||
label = "Avatar size",
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(CircleShape)
|
||||
.clickable(enabled = onClick != null && !selected, onClick = { onClick?.invoke() }),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
AvatarOutline(
|
||||
color = color,
|
||||
modifier = Modifier.size(avatarSize),
|
||||
) {
|
||||
AvatarPlaceholder(
|
||||
displayName = name,
|
||||
)
|
||||
// TODO: Add image loading
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AvatarOutline(
|
||||
color: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.clip(CircleShape)
|
||||
.border(2.dp, color, CircleShape)
|
||||
.padding(2.dp),
|
||||
color = color.copy(alpha = 0.3f),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.border(2.dp, MainTheme.colors.surfaceContainerLowest, CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AvatarPlaceholder(
|
||||
displayName: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TextTitleMedium(
|
||||
text = extractNameInitials(displayName).uppercase(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
private fun extractNameInitials(displayName: String): String {
|
||||
return displayName.take(2)
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
package net.thunderbird.feature.account.avatar.ui
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.Surface
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleLarge
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import app.k9mail.core.ui.compose.theme2.toSurfaceContainer
|
||||
|
||||
private const val AVATAR_ALPHA = 0.2f
|
||||
|
||||
@Composable
|
||||
fun AvatarOutlined(
|
||||
color: Color,
|
||||
name: String,
|
||||
modifier: Modifier = Modifier,
|
||||
size: AvatarSize = AvatarSize.MEDIUM,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val avatarColor = calculateAvatarColor(color)
|
||||
val containerColor = avatarColor.toSurfaceContainer(alpha = AVATAR_ALPHA)
|
||||
|
||||
AvatarLayout(
|
||||
color = containerColor,
|
||||
borderColor = avatarColor,
|
||||
onClick = onClick,
|
||||
modifier = modifier.size(getAvatarSize(size)),
|
||||
) {
|
||||
AvatarPlaceholder(
|
||||
color = avatarColor,
|
||||
displayName = name,
|
||||
size = size,
|
||||
)
|
||||
// TODO: Add image loading
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AvatarLayout(
|
||||
color: Color,
|
||||
borderColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
color = color,
|
||||
shape = CircleShape,
|
||||
modifier = modifier
|
||||
.border(
|
||||
width = 2.dp,
|
||||
shape = CircleShape,
|
||||
color = borderColor,
|
||||
)
|
||||
.clickable(
|
||||
enabled = onClick != null,
|
||||
onClick = { onClick?.invoke() },
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AvatarPlaceholder(
|
||||
color: Color,
|
||||
displayName: String,
|
||||
size: AvatarSize,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (size) {
|
||||
AvatarSize.MEDIUM -> {
|
||||
TextTitleMedium(
|
||||
text = extractNameInitials(displayName).uppercase(),
|
||||
color = color,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
AvatarSize.LARGE -> {
|
||||
TextTitleLarge(
|
||||
text = extractNameInitials(displayName).uppercase(),
|
||||
color = color,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAvatarSize(size: AvatarSize): Dp {
|
||||
return when (size) {
|
||||
AvatarSize.MEDIUM -> MainTheme.sizes.iconAvatar
|
||||
AvatarSize.LARGE -> MainTheme.sizes.large
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractNameInitials(displayName: String): String {
|
||||
return displayName.take(2)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package net.thunderbird.feature.account.avatar.ui
|
||||
|
||||
enum class AvatarSize {
|
||||
MEDIUM,
|
||||
LARGE,
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package net.thunderbird.feature.account.avatar.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import app.k9mail.core.ui.compose.theme2.toHarmonizedColor
|
||||
|
||||
@Composable
|
||||
internal fun calculateAvatarColor(accountColor: Color): Color {
|
||||
return if (accountColor == Color.Unspecified) {
|
||||
MainTheme.colors.tertiary
|
||||
} else {
|
||||
accountColor.toHarmonizedColor(MainTheme.colors.surface)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package net.thunderbird.feature.account.avatar
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlin.test.Test
|
||||
|
||||
class DefaultAvatarMonogramCreatorTest {
|
||||
|
||||
private val testSubject = DefaultAvatarMonogramCreator()
|
||||
|
||||
@Test
|
||||
fun `create returns correct monogram for name`() {
|
||||
val name = "John Doe"
|
||||
val expectedMonogram = "JO"
|
||||
|
||||
val result = testSubject.create(name, null)
|
||||
|
||||
assertThat(result).isEqualTo(expectedMonogram)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create returns correct monogram for email`() {
|
||||
val email = "test@example.com"
|
||||
val expectedMonogram = "TE"
|
||||
|
||||
val result = testSubject.create(null, email)
|
||||
|
||||
assertThat(result).isEqualTo(expectedMonogram)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create returns default monogram for null or empty inputs`() {
|
||||
val expectedMonogram = "XX"
|
||||
|
||||
val resultWithNulls = testSubject.create(null, null)
|
||||
assertThat(resultWithNulls).isEqualTo(expectedMonogram)
|
||||
|
||||
val resultWithEmptyStrings = testSubject.create("", "")
|
||||
assertThat(resultWithEmptyStrings).isEqualTo(expectedMonogram)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue