Repo created
This commit is contained in:
parent
75dc487a7a
commit
39c29d175b
6317 changed files with 388324 additions and 2 deletions
15
feature/account/api/build.gradle.kts
Normal file
15
feature/account/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.kmp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.thunderbird.feature.account"
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
api(projects.core.architecture.api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package net.thunderbird.feature.account
|
||||
|
||||
import net.thunderbird.core.architecture.model.Identifiable
|
||||
|
||||
/**
|
||||
* Interface representing an account by its unique identifier [AccountId].
|
||||
*/
|
||||
interface Account : Identifiable<Account>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package net.thunderbird.feature.account
|
||||
|
||||
import net.thunderbird.core.architecture.model.Id
|
||||
|
||||
/**
|
||||
* Represents a unique identifier for an [Account].
|
||||
*/
|
||||
typealias AccountId = Id<Account>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package net.thunderbird.feature.account
|
||||
|
||||
import net.thunderbird.core.architecture.model.BaseIdFactory
|
||||
|
||||
/**
|
||||
* Factory object for creating unique identifiers for [Account] instances.
|
||||
*/
|
||||
object AccountIdFactory : BaseIdFactory<Account>()
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package net.thunderbird.feature.account.profile
|
||||
|
||||
/**
|
||||
* Sealed interface representing the avatar of an account.
|
||||
*/
|
||||
sealed interface AccountAvatar {
|
||||
data class Monogram(
|
||||
val value: String,
|
||||
) : AccountAvatar
|
||||
|
||||
data class Image(
|
||||
val uri: String,
|
||||
) : AccountAvatar
|
||||
|
||||
data class Icon(
|
||||
val name: String,
|
||||
) : AccountAvatar
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package net.thunderbird.feature.account.profile
|
||||
|
||||
import net.thunderbird.feature.account.Account
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
|
||||
/**
|
||||
* Data class representing an account profile.
|
||||
*
|
||||
* @property id The unique identifier of the account profile.
|
||||
* @property name The name of the account.
|
||||
* @property color The color associated with the account.
|
||||
* @property avatar The [AccountAvatar] representing the avatar of the account.
|
||||
*/
|
||||
data class AccountProfile(
|
||||
override val id: AccountId,
|
||||
val name: String,
|
||||
val color: Int,
|
||||
val avatar: AccountAvatar,
|
||||
) : Account
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package net.thunderbird.feature.account.profile
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
|
||||
interface AccountProfileRepository {
|
||||
|
||||
fun getById(accountId: AccountId): Flow<AccountProfile?>
|
||||
|
||||
suspend fun update(accountProfile: AccountProfile)
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package net.thunderbird.feature.account
|
||||
|
||||
import assertk.Assert
|
||||
import assertk.assertFailure
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.hasMessage
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isInstanceOf
|
||||
import assertk.assertions.isNotEqualTo
|
||||
import kotlin.test.Test
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
class AccountIdFactoryTest {
|
||||
|
||||
@Test
|
||||
fun `create should return AccountId with the same id`() {
|
||||
val id = "123e4567-e89b-12d3-a456-426614174000"
|
||||
|
||||
val result = AccountIdFactory.of(id)
|
||||
|
||||
assertThat(result.asRaw()).isEqualTo(id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create should throw IllegalArgumentException when id is invalid`() {
|
||||
val id = "invalid"
|
||||
|
||||
val result = assertFailure {
|
||||
AccountIdFactory.of(id)
|
||||
}
|
||||
|
||||
result.hasMessage(
|
||||
"Expected either a 36-char string in the standard hex-and-dash UUID format or a 32-char " +
|
||||
"hexadecimal string, but was \"invalid\" of length 7",
|
||||
)
|
||||
result.isInstanceOf<IllegalArgumentException>()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `new should return AccountId with a uuid`() {
|
||||
val result = AccountIdFactory.create()
|
||||
|
||||
assertThat(result.asRaw()).isUuid()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create should return AccountId with unique ids`() {
|
||||
val ids = List(10) { AccountIdFactory.create().asRaw() }
|
||||
|
||||
ids.forEachIndexed { index, id ->
|
||||
ids.drop(index + 1).forEach { otherId ->
|
||||
assertThat(id).isNotEqualTo(otherId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
private fun Assert<String>.isUuid() = given { actual ->
|
||||
Uuid.parse(actual)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
17
feature/account/common/build.gradle.kts
Normal file
17
feature/account/common/build.gradle.kts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.androidCompose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.k9mail.feature.account.common"
|
||||
resourcePrefix = "account_common_"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.ui.compose.designsystem)
|
||||
implementation(projects.core.common)
|
||||
|
||||
implementation(projects.mail.common)
|
||||
|
||||
testImplementation(projects.core.ui.compose.testing)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package app.k9mail.feature.account.common.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import app.k9mail.core.ui.compose.common.annotation.PreviewDevices
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
|
||||
|
||||
@Composable
|
||||
@PreviewDevices
|
||||
internal fun AccountTopAppBarPreview() {
|
||||
PreviewWithThemes {
|
||||
AccountTopAppBar(
|
||||
title = "Title",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package app.k9mail.feature.account.common.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AppTitleTopHeaderPreview() {
|
||||
PreviewWithThemes {
|
||||
AppTitleTopHeader(title = "Title")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package app.k9mail.feature.account.common.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun ContentListViewPreview() {
|
||||
PreviewWithThemes {
|
||||
ContentListView {
|
||||
item {
|
||||
TextTitleMedium("Item 1")
|
||||
}
|
||||
item {
|
||||
TextTitleMedium("Item 2")
|
||||
}
|
||||
item {
|
||||
TextTitleMedium("Item 3")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package app.k9mail.feature.account.common.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import app.k9mail.core.ui.compose.common.annotation.PreviewDevices
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
|
||||
|
||||
@Composable
|
||||
@PreviewDevices
|
||||
internal fun WizardNavigationBarPreview() {
|
||||
PreviewWithThemes {
|
||||
WizardNavigationBar(
|
||||
onNextClick = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@PreviewDevices
|
||||
internal fun WizardNavigationBarDisabledPreview() {
|
||||
PreviewWithThemes {
|
||||
WizardNavigationBar(
|
||||
onNextClick = {},
|
||||
onBackClick = {},
|
||||
state = WizardNavigationBarState(
|
||||
isNextEnabled = false,
|
||||
isBackEnabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@PreviewDevices
|
||||
internal fun WizardNavigationBarHideNextPreview() {
|
||||
PreviewWithThemes {
|
||||
WizardNavigationBar(
|
||||
onNextClick = {},
|
||||
onBackClick = {},
|
||||
state = WizardNavigationBarState(
|
||||
showNext = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@PreviewDevices
|
||||
internal fun WizardNavigationBarHideBackPreview() {
|
||||
PreviewWithThemes {
|
||||
WizardNavigationBar(
|
||||
onNextClick = {},
|
||||
onBackClick = {},
|
||||
state = WizardNavigationBarState(
|
||||
showBack = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package app.k9mail.feature.account.common.ui.fake
|
||||
|
||||
import app.k9mail.feature.account.common.domain.AccountDomainContract
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountDisplayOptions
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountState
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountSyncOptions
|
||||
import app.k9mail.feature.account.common.domain.entity.AuthorizationState
|
||||
import app.k9mail.feature.account.common.domain.entity.MailConnectionSecurity
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderSettings
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class FakeAccountStateRepository : AccountDomainContract.AccountStateRepository {
|
||||
|
||||
override fun getState(): AccountState = AccountState(
|
||||
emailAddress = "test@example.com",
|
||||
incomingServerSettings = ServerSettings(
|
||||
type = "imap",
|
||||
host = "imap.example.com",
|
||||
port = 993,
|
||||
connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
authenticationType = AuthType.PLAIN,
|
||||
username = "test",
|
||||
password = "password",
|
||||
clientCertificateAlias = null,
|
||||
),
|
||||
outgoingServerSettings = ServerSettings(
|
||||
type = "smtp",
|
||||
host = "smtp.example.com",
|
||||
port = 465,
|
||||
connectionSecurity = MailConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
authenticationType = AuthType.PLAIN,
|
||||
username = "test",
|
||||
password = "password",
|
||||
clientCertificateAlias = null,
|
||||
),
|
||||
)
|
||||
|
||||
override fun setState(accountState: AccountState) = Unit
|
||||
|
||||
override fun setEmailAddress(emailAddress: String) = Unit
|
||||
|
||||
override fun setIncomingServerSettings(serverSettings: ServerSettings) = Unit
|
||||
|
||||
override fun setOutgoingServerSettings(serverSettings: ServerSettings) = Unit
|
||||
|
||||
override fun setAuthorizationState(authorizationState: AuthorizationState) = Unit
|
||||
|
||||
override fun setSpecialFolderSettings(specialFolderSettings: SpecialFolderSettings) = Unit
|
||||
|
||||
override fun setDisplayOptions(displayOptions: AccountDisplayOptions) = Unit
|
||||
|
||||
override fun setSyncOptions(syncOptions: AccountSyncOptions) = Unit
|
||||
|
||||
override fun clear() = Unit
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package app.k9mail.feature.account.common
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountState
|
||||
|
||||
interface AccountCommonExternalContract {
|
||||
|
||||
fun interface AccountStateLoader {
|
||||
suspend fun loadAccountState(accountUuid: String): AccountState?
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package app.k9mail.feature.account.common
|
||||
|
||||
import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
|
||||
import app.k9mail.feature.account.common.domain.AccountDomainContract
|
||||
import com.fsck.k9.mail.oauth.AuthStateStorage
|
||||
import net.thunderbird.core.common.coreCommonModule
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.binds
|
||||
import org.koin.dsl.module
|
||||
|
||||
val featureAccountCommonModule: Module = module {
|
||||
includes(coreCommonModule)
|
||||
|
||||
single {
|
||||
InMemoryAccountStateRepository()
|
||||
}.binds(arrayOf(AccountDomainContract.AccountStateRepository::class, AuthStateStorage::class))
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package app.k9mail.feature.account.common.data
|
||||
|
||||
import app.k9mail.feature.account.common.domain.AccountDomainContract
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountDisplayOptions
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountState
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountSyncOptions
|
||||
import app.k9mail.feature.account.common.domain.entity.AuthorizationState
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderSettings
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.oauth.AuthStateStorage
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class InMemoryAccountStateRepository(
|
||||
private var state: AccountState = AccountState(),
|
||||
) : AccountDomainContract.AccountStateRepository, AuthStateStorage {
|
||||
|
||||
override fun getState(): AccountState {
|
||||
return state
|
||||
}
|
||||
|
||||
override fun setState(accountState: AccountState) {
|
||||
state = accountState
|
||||
}
|
||||
|
||||
override fun setEmailAddress(emailAddress: String) {
|
||||
state = state.copy(emailAddress = emailAddress)
|
||||
}
|
||||
|
||||
override fun setIncomingServerSettings(serverSettings: ServerSettings) {
|
||||
state = state.copy(incomingServerSettings = serverSettings)
|
||||
}
|
||||
|
||||
override fun setOutgoingServerSettings(serverSettings: ServerSettings) {
|
||||
state = state.copy(outgoingServerSettings = serverSettings)
|
||||
}
|
||||
|
||||
override fun setAuthorizationState(authorizationState: AuthorizationState) {
|
||||
state = state.copy(authorizationState = authorizationState)
|
||||
}
|
||||
|
||||
override fun setSpecialFolderSettings(specialFolderSettings: SpecialFolderSettings) {
|
||||
state = state.copy(specialFolderSettings = specialFolderSettings)
|
||||
}
|
||||
|
||||
override fun setDisplayOptions(displayOptions: AccountDisplayOptions) {
|
||||
state = state.copy(displayOptions = displayOptions)
|
||||
}
|
||||
|
||||
override fun setSyncOptions(syncOptions: AccountSyncOptions) {
|
||||
state = state.copy(syncOptions = syncOptions)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
state = AccountState()
|
||||
}
|
||||
|
||||
override fun getAuthorizationState(): String? {
|
||||
return state.authorizationState?.value
|
||||
}
|
||||
|
||||
override fun updateAuthorizationState(authorizationState: String?) {
|
||||
state = state.copy(authorizationState = AuthorizationState(authorizationState))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package app.k9mail.feature.account.common.domain
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountDisplayOptions
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountState
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountSyncOptions
|
||||
import app.k9mail.feature.account.common.domain.entity.AuthorizationState
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderSettings
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
|
||||
interface AccountDomainContract {
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AccountStateRepository {
|
||||
fun getState(): AccountState
|
||||
|
||||
fun setState(accountState: AccountState)
|
||||
|
||||
fun setEmailAddress(emailAddress: String)
|
||||
|
||||
fun setIncomingServerSettings(serverSettings: ServerSettings)
|
||||
|
||||
fun setOutgoingServerSettings(serverSettings: ServerSettings)
|
||||
|
||||
fun setAuthorizationState(authorizationState: AuthorizationState)
|
||||
|
||||
fun setSpecialFolderSettings(specialFolderSettings: SpecialFolderSettings)
|
||||
|
||||
fun setDisplayOptions(displayOptions: AccountDisplayOptions)
|
||||
|
||||
fun setSyncOptions(syncOptions: AccountSyncOptions)
|
||||
|
||||
fun clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
|
||||
data class Account(
|
||||
val uuid: String,
|
||||
val emailAddress: String,
|
||||
val incomingServerSettings: ServerSettings,
|
||||
val outgoingServerSettings: ServerSettings,
|
||||
val authorizationState: String?,
|
||||
val specialFolderSettings: SpecialFolderSettings?,
|
||||
val options: AccountOptions,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
data class AccountDisplayOptions(
|
||||
val accountName: String,
|
||||
val displayName: String,
|
||||
val emailSignature: String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
data class AccountOptions(
|
||||
val accountName: String,
|
||||
val displayName: String,
|
||||
val emailSignature: String?,
|
||||
val checkFrequencyInMinutes: Int,
|
||||
val messageDisplayCount: Int,
|
||||
val showNotification: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
|
||||
data class AccountState(
|
||||
val uuid: String? = null,
|
||||
val emailAddress: String? = null,
|
||||
val incomingServerSettings: ServerSettings? = null,
|
||||
val outgoingServerSettings: ServerSettings? = null,
|
||||
val authorizationState: AuthorizationState? = null,
|
||||
val specialFolderSettings: SpecialFolderSettings? = null,
|
||||
val displayOptions: AccountDisplayOptions? = null,
|
||||
val syncOptions: AccountSyncOptions? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
data class AccountSyncOptions(
|
||||
val checkFrequencyInMinutes: Int,
|
||||
val messageDisplayCount: Int,
|
||||
val showNotification: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
enum class AuthenticationType(
|
||||
val isUsernameRequired: Boolean,
|
||||
val isPasswordRequired: Boolean,
|
||||
) {
|
||||
None(
|
||||
isUsernameRequired = false,
|
||||
isPasswordRequired = false,
|
||||
),
|
||||
PasswordCleartext(
|
||||
isUsernameRequired = true,
|
||||
isPasswordRequired = true,
|
||||
),
|
||||
PasswordEncrypted(
|
||||
isUsernameRequired = true,
|
||||
isPasswordRequired = true,
|
||||
),
|
||||
ClientCertificate(
|
||||
isUsernameRequired = true,
|
||||
isPasswordRequired = false,
|
||||
),
|
||||
OAuth2(
|
||||
isUsernameRequired = true,
|
||||
isPasswordRequired = false,
|
||||
),
|
||||
;
|
||||
|
||||
companion object {
|
||||
val DEFAULT = PasswordCleartext
|
||||
fun all() = entries.toImmutableList()
|
||||
|
||||
fun outgoing() = all()
|
||||
}
|
||||
}
|
||||
|
||||
fun AuthenticationType.toAuthType(): AuthType {
|
||||
return when (this) {
|
||||
AuthenticationType.None -> AuthType.NONE
|
||||
AuthenticationType.PasswordCleartext -> AuthType.PLAIN
|
||||
AuthenticationType.PasswordEncrypted -> AuthType.CRAM_MD5
|
||||
AuthenticationType.ClientCertificate -> AuthType.EXTERNAL
|
||||
AuthenticationType.OAuth2 -> AuthType.XOAUTH2
|
||||
}
|
||||
}
|
||||
|
||||
fun AuthType.toAuthenticationType(): AuthenticationType {
|
||||
return when (this) {
|
||||
AuthType.PLAIN -> AuthenticationType.PasswordCleartext
|
||||
AuthType.CRAM_MD5 -> AuthenticationType.PasswordEncrypted
|
||||
AuthType.EXTERNAL -> AuthenticationType.ClientCertificate
|
||||
AuthType.XOAUTH2 -> AuthenticationType.OAuth2
|
||||
AuthType.NONE -> AuthenticationType.None
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
data class AuthorizationState(
|
||||
val value: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.ConnectionSecurity.None
|
||||
import app.k9mail.feature.account.common.domain.entity.ConnectionSecurity.StartTLS
|
||||
import app.k9mail.feature.account.common.domain.entity.ConnectionSecurity.TLS
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
enum class ConnectionSecurity {
|
||||
None,
|
||||
StartTLS,
|
||||
TLS,
|
||||
;
|
||||
|
||||
companion object {
|
||||
val DEFAULT = TLS
|
||||
fun all() = entries.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun ConnectionSecurity.toMailConnectionSecurity(): MailConnectionSecurity {
|
||||
return when (this) {
|
||||
None -> MailConnectionSecurity.NONE
|
||||
StartTLS -> MailConnectionSecurity.STARTTLS_REQUIRED
|
||||
TLS -> MailConnectionSecurity.SSL_TLS_REQUIRED
|
||||
}
|
||||
}
|
||||
|
||||
fun MailConnectionSecurity.toConnectionSecurity(): ConnectionSecurity {
|
||||
return when (this) {
|
||||
MailConnectionSecurity.NONE -> None
|
||||
MailConnectionSecurity.STARTTLS_REQUIRED -> StartTLS
|
||||
MailConnectionSecurity.SSL_TLS_REQUIRED -> TLS
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun ConnectionSecurity.toSmtpDefaultPort(): Long {
|
||||
return when (this) {
|
||||
None -> 587
|
||||
StartTLS -> 587
|
||||
TLS -> 465
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun ConnectionSecurity.toImapDefaultPort(): Long {
|
||||
return when (this) {
|
||||
None -> 143
|
||||
StartTLS -> 143
|
||||
TLS -> 993
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun ConnectionSecurity.toPop3DefaultPort(): Long {
|
||||
return when (this) {
|
||||
None -> 110
|
||||
StartTLS -> 110
|
||||
TLS -> 995
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
enum class IncomingProtocolType(
|
||||
val defaultName: String,
|
||||
val defaultConnectionSecurity: ConnectionSecurity,
|
||||
) {
|
||||
IMAP("imap", ConnectionSecurity.TLS),
|
||||
POP3("pop3", ConnectionSecurity.TLS),
|
||||
;
|
||||
|
||||
companion object {
|
||||
val DEFAULT = IMAP
|
||||
|
||||
fun all() = entries.toImmutableList()
|
||||
|
||||
fun fromName(name: String): IncomingProtocolType {
|
||||
return entries.find { it.defaultName == name } ?: throw IllegalArgumentException("Unknown protocol: $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun IncomingProtocolType.toDefaultPort(connectionSecurity: ConnectionSecurity): Long {
|
||||
return when (this) {
|
||||
IncomingProtocolType.IMAP -> connectionSecurity.toImapDefaultPort()
|
||||
IncomingProtocolType.POP3 -> connectionSecurity.toPop3DefaultPort()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
/**
|
||||
* Enum representing the mode a user is interacting with an account or setting.
|
||||
*/
|
||||
enum class InteractionMode {
|
||||
Create,
|
||||
Edit,
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
typealias MailConnectionSecurity = com.fsck.k9.mail.ConnectionSecurity
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
enum class OutgoingProtocolType(
|
||||
val defaultName: String,
|
||||
val defaultConnectionSecurity: ConnectionSecurity,
|
||||
) {
|
||||
SMTP("smtp", ConnectionSecurity.TLS),
|
||||
;
|
||||
|
||||
companion object {
|
||||
val DEFAULT = SMTP
|
||||
|
||||
fun all() = entries.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun OutgoingProtocolType.toDefaultPort(connectionSecurity: ConnectionSecurity): Long {
|
||||
return when (this) {
|
||||
OutgoingProtocolType.SMTP -> connectionSecurity.toSmtpDefaultPort()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
import com.fsck.k9.mail.folders.RemoteFolder
|
||||
|
||||
sealed interface SpecialFolderOption {
|
||||
data class None(
|
||||
val isAutomatic: Boolean = false,
|
||||
) : SpecialFolderOption
|
||||
|
||||
data class Regular(
|
||||
val remoteFolder: RemoteFolder,
|
||||
) : SpecialFolderOption
|
||||
|
||||
data class Special(
|
||||
val isAutomatic: Boolean = false,
|
||||
val remoteFolder: RemoteFolder,
|
||||
) : SpecialFolderOption
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
data class SpecialFolderOptions(
|
||||
val archiveSpecialFolderOptions: List<SpecialFolderOption>,
|
||||
val draftsSpecialFolderOptions: List<SpecialFolderOption>,
|
||||
val sentSpecialFolderOptions: List<SpecialFolderOption>,
|
||||
val spamSpecialFolderOptions: List<SpecialFolderOption>,
|
||||
val trashSpecialFolderOptions: List<SpecialFolderOption>,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package app.k9mail.feature.account.common.domain.entity
|
||||
|
||||
data class SpecialFolderSettings(
|
||||
val archiveSpecialFolderOption: SpecialFolderOption,
|
||||
val draftsSpecialFolderOption: SpecialFolderOption,
|
||||
val sentSpecialFolderOption: SpecialFolderOption,
|
||||
val spamSpecialFolderOption: SpecialFolderOption,
|
||||
val trashSpecialFolderOption: SpecialFolderOption,
|
||||
)
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package app.k9mail.feature.account.common.domain.input
|
||||
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationError
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
class BooleanInputField(
|
||||
override val value: Boolean? = null,
|
||||
override val error: ValidationError? = null,
|
||||
override val isValid: Boolean = false,
|
||||
) : InputField<Boolean?> {
|
||||
override fun updateValue(value: Boolean?): BooleanInputField {
|
||||
return BooleanInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateError(error: ValidationError?): BooleanInputField {
|
||||
return BooleanInputField(
|
||||
value = value,
|
||||
error = error,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateValidity(isValid: Boolean): BooleanInputField {
|
||||
if (isValid == this.isValid) return this
|
||||
|
||||
return BooleanInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = isValid,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateFromValidationResult(result: ValidationResult): BooleanInputField {
|
||||
return when (result) {
|
||||
is ValidationResult.Success -> BooleanInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = true,
|
||||
)
|
||||
|
||||
is ValidationResult.Failure -> BooleanInputField(
|
||||
value = value,
|
||||
error = result.error,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as BooleanInputField
|
||||
|
||||
if (value != other.value) return false
|
||||
if (error != other.error) return false
|
||||
return isValid == other.isValid
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = value?.hashCode() ?: 0
|
||||
result = 31 * result + (error?.hashCode() ?: 0)
|
||||
result = 31 * result + isValid.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "BooleanInputField(value=$value, error=$error, isValid=$isValid)"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package app.k9mail.feature.account.common.domain.input
|
||||
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationError
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
/**
|
||||
* InputField is an interface defining the state of an input field.
|
||||
*
|
||||
* @param T The type of the value the input field holds.
|
||||
*/
|
||||
interface InputField<T> {
|
||||
val value: T
|
||||
val error: ValidationError?
|
||||
val isValid: Boolean
|
||||
|
||||
/**
|
||||
* Updates the current value of the input field.
|
||||
*
|
||||
* @param value The new value to be set for the input field.
|
||||
* @return a new InputField instance with the updated value.
|
||||
*/
|
||||
fun updateValue(value: T): InputField<T>
|
||||
|
||||
/**
|
||||
* Updates the current error of the input field.
|
||||
*
|
||||
* @param error The new error to be set for the input field.
|
||||
*/
|
||||
fun updateError(error: ValidationError?): InputField<T>
|
||||
|
||||
/**
|
||||
* Updates the current validity of the input field.
|
||||
*
|
||||
* @param isValid The new validity to be set for the input field.
|
||||
*/
|
||||
fun updateValidity(isValid: Boolean): InputField<T>
|
||||
|
||||
/**
|
||||
* Checks if the input field currently has an error.
|
||||
*
|
||||
* @return a Boolean indicating whether the input field has an error.
|
||||
*/
|
||||
fun hasError(): Boolean {
|
||||
return error != null
|
||||
}
|
||||
|
||||
fun updateFromValidationResult(result: ValidationResult): InputField<T>
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package app.k9mail.feature.account.common.domain.input
|
||||
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationError
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
class NumberInputField(
|
||||
override val value: Long? = null,
|
||||
override val error: ValidationError? = null,
|
||||
override val isValid: Boolean = false,
|
||||
) : InputField<Long?> {
|
||||
|
||||
override fun updateValue(value: Long?): NumberInputField {
|
||||
return NumberInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateError(error: ValidationError?): NumberInputField {
|
||||
return NumberInputField(
|
||||
value = value,
|
||||
error = error,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateValidity(isValid: Boolean): NumberInputField {
|
||||
if (isValid == this.isValid) return this
|
||||
|
||||
return NumberInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = isValid,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateFromValidationResult(result: ValidationResult): NumberInputField {
|
||||
return when (result) {
|
||||
is ValidationResult.Success -> NumberInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = true,
|
||||
)
|
||||
|
||||
is ValidationResult.Failure -> NumberInputField(
|
||||
value = value,
|
||||
error = result.error,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as NumberInputField
|
||||
|
||||
if (value != other.value) return false
|
||||
if (error != other.error) return false
|
||||
return isValid == other.isValid
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = value?.hashCode() ?: 0
|
||||
result = 31 * result + (error?.hashCode() ?: 0)
|
||||
result = 31 * result + isValid.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "NumberInputField(value=$value, error=$error, isValid=$isValid)"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package app.k9mail.feature.account.common.domain.input
|
||||
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationError
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
class StringInputField(
|
||||
override val value: String = "",
|
||||
override val error: ValidationError? = null,
|
||||
override val isValid: Boolean = false,
|
||||
) : InputField<String> {
|
||||
|
||||
override fun updateValue(value: String): StringInputField {
|
||||
return StringInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateError(error: ValidationError?): StringInputField {
|
||||
return StringInputField(
|
||||
value = value,
|
||||
error = error,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateValidity(isValid: Boolean): StringInputField {
|
||||
if (isValid == this.isValid) return this
|
||||
|
||||
return StringInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = isValid,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateFromValidationResult(result: ValidationResult): StringInputField {
|
||||
return when (result) {
|
||||
is ValidationResult.Success -> StringInputField(
|
||||
value = value,
|
||||
error = null,
|
||||
isValid = true,
|
||||
)
|
||||
|
||||
is ValidationResult.Failure -> StringInputField(
|
||||
value = value,
|
||||
error = result.error,
|
||||
isValid = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as StringInputField
|
||||
|
||||
if (value != other.value) return false
|
||||
if (error != other.error) return false
|
||||
return isValid == other.isValid
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = value.hashCode()
|
||||
result = 31 * result + (error?.hashCode() ?: 0)
|
||||
result = 31 * result + isValid.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "StringInputField(value='$value', error=$error, isValid=$isValid)"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package app.k9mail.feature.account.common.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.k9mail.core.ui.compose.designsystem.organism.TopAppBar
|
||||
|
||||
/**
|
||||
* Top app bar for the account screens.
|
||||
*/
|
||||
@Composable
|
||||
fun AccountTopAppBar(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TopAppBar(
|
||||
title = title,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package app.k9mail.feature.account.common.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextDisplayMediumAutoResize
|
||||
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
|
||||
private const val TITLE_ICON_SIZE_DP = 56
|
||||
|
||||
@Composable
|
||||
fun AppTitleTopHeader(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ResponsiveWidthContainer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = MainTheme.spacings.quadruple,
|
||||
bottom = MainTheme.spacings.default,
|
||||
)
|
||||
.then(modifier),
|
||||
) { contentPadding ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = MainTheme.spacings.half,
|
||||
end = MainTheme.spacings.quadruple,
|
||||
)
|
||||
.padding(contentPadding)
|
||||
.then(modifier),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = MainTheme.images.logo),
|
||||
modifier = Modifier
|
||||
.padding(all = MainTheme.spacings.default)
|
||||
.padding(end = MainTheme.spacings.default)
|
||||
.size(TITLE_ICON_SIZE_DP.dp),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
TextDisplayMediumAutoResize(text = title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package app.k9mail.feature.account.common.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
|
||||
@Composable
|
||||
fun ContentListView(
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(),
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(MainTheme.spacings.default),
|
||||
items: LazyListScope.() -> Unit,
|
||||
) {
|
||||
ResponsiveWidthContainer(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.fillMaxWidth()
|
||||
.then(modifier),
|
||||
) { contentPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding(),
|
||||
contentPadding = contentPadding,
|
||||
horizontalAlignment = horizontalAlignment,
|
||||
verticalArrangement = verticalArrangement,
|
||||
) {
|
||||
items()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package app.k9mail.feature.account.common.ui
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.InteractionMode
|
||||
|
||||
/**
|
||||
* Interface for screens that can be used in different interaction modes.
|
||||
*/
|
||||
interface WithInteractionMode {
|
||||
val mode: InteractionMode
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package app.k9mail.feature.account.common.ui
|
||||
|
||||
object WizardConstants {
|
||||
const val CONTINUE_NEXT_DELAY = 500L
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package app.k9mail.feature.account.common.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonFilled
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonOutlined
|
||||
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import app.k9mail.feature.account.common.R
|
||||
import net.thunderbird.core.ui.compose.common.modifier.testTagAsResourceId
|
||||
|
||||
@Composable
|
||||
fun WizardNavigationBar(
|
||||
onNextClick: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
nextButtonText: String = stringResource(id = R.string.account_common_button_next),
|
||||
backButtonText: String = stringResource(id = R.string.account_common_button_back),
|
||||
state: WizardNavigationBarState = WizardNavigationBarState(),
|
||||
) {
|
||||
ResponsiveWidthContainer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(modifier),
|
||||
) { contentPadding ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = MainTheme.spacings.quadruple,
|
||||
top = MainTheme.spacings.default,
|
||||
end = MainTheme.spacings.quadruple,
|
||||
bottom = MainTheme.spacings.double,
|
||||
)
|
||||
.padding(contentPadding)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = getHorizontalArrangement(state),
|
||||
) {
|
||||
if (state.showBack) {
|
||||
ButtonOutlined(
|
||||
text = backButtonText,
|
||||
onClick = onBackClick,
|
||||
enabled = state.isBackEnabled,
|
||||
modifier = Modifier.testTagAsResourceId("account_setup_back_button"),
|
||||
)
|
||||
}
|
||||
if (state.showNext) {
|
||||
ButtonFilled(
|
||||
text = nextButtonText,
|
||||
onClick = onNextClick,
|
||||
enabled = state.isNextEnabled,
|
||||
modifier = Modifier.testTagAsResourceId("account_setup_next_button"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHorizontalArrangement(state: WizardNavigationBarState): Arrangement.Horizontal {
|
||||
return if (state.showNext && state.showBack) {
|
||||
Arrangement.SpaceBetween
|
||||
} else if (state.showNext) {
|
||||
Arrangement.End
|
||||
} else {
|
||||
Arrangement.Start
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package app.k9mail.feature.account.common.ui
|
||||
|
||||
data class WizardNavigationBarState(
|
||||
val isNextEnabled: Boolean = true,
|
||||
val showNext: Boolean = true,
|
||||
val isBackEnabled: Boolean = true,
|
||||
val showBack: Boolean = true,
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package app.k9mail.feature.account.common.ui.item
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.ErrorView
|
||||
|
||||
@Composable
|
||||
fun LazyItemScope.ErrorItem(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
message: String? = null,
|
||||
onRetry: () -> Unit = { },
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
) {
|
||||
ErrorView(
|
||||
title = title,
|
||||
message = message,
|
||||
onRetry = onRetry,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package app.k9mail.feature.account.common.ui.item
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
|
||||
@Composable
|
||||
fun defaultHeadlineItemPadding() = PaddingValues(
|
||||
start = MainTheme.spacings.quadruple,
|
||||
top = MainTheme.spacings.triple,
|
||||
end = MainTheme.spacings.quadruple,
|
||||
bottom = MainTheme.spacings.default,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun defaultItemPadding() = PaddingValues(
|
||||
horizontal = MainTheme.spacings.quadruple,
|
||||
vertical = MainTheme.spacings.zero,
|
||||
)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package app.k9mail.feature.account.common.ui.item
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun LazyItemScope.ListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
contentPaddingValues: PaddingValues = defaultItemPadding(),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(contentPaddingValues)
|
||||
.animateItem()
|
||||
.fillMaxWidth()
|
||||
.then(modifier),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package app.k9mail.feature.account.common.ui.item
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.LoadingView
|
||||
|
||||
@Composable
|
||||
fun LazyItemScope.LoadingItem(
|
||||
modifier: Modifier = Modifier,
|
||||
message: String? = null,
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
) {
|
||||
LoadingView(
|
||||
message = message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_error_server_message">قام الخادم بإرجاع الرسالة التالية:\n%s</string>
|
||||
<string name="account_common_button_next">التالي</string>
|
||||
<string name="account_common_button_back">السابق</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Növbəti</string>
|
||||
<string name="account_common_button_back">Geri</string>
|
||||
<string name="account_common_error_server_message">Server aşağıdakı məlumatı verdi: \n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Далей</string>
|
||||
<string name="account_common_button_back">Назад</string>
|
||||
<string name="account_common_error_server_message">Сервер вярнуў наступнае паведамленне:\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Напред</string>
|
||||
<string name="account_common_button_back">Назад</string>
|
||||
<string name="account_common_error_server_message">Сървърът връща следното съобщение\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_back">পিছনে</string>
|
||||
<string name="account_common_error_server_message">সার্ভার এই বার্তাটি পাঠিয়েছে:\n%s</string>
|
||||
<string name="account_common_button_next">পরবর্তী</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Da heul</string>
|
||||
<string name="account_common_button_back">Kent</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Sljedeći</string>
|
||||
<string name="account_common_button_back">Nazad</string>
|
||||
<string name="account_common_error_server_message">Server je odgovorio ovom porukom:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Següent</string>
|
||||
<string name="account_common_button_back">Enrere</string>
|
||||
<string name="account_common_error_server_message">El servidor ha retornat en següent missatge:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Seguente</string>
|
||||
<string name="account_common_button_back">Ritornu</string>
|
||||
<string name="account_common_error_server_message">U servitore hà mandatu quessu messaghju :
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Další</string>
|
||||
<string name="account_common_button_back">Zpět</string>
|
||||
<string name="account_common_error_server_message">Server vrátil následující zprávu:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Nesaf</string>
|
||||
<string name="account_common_button_back">Nôl</string>
|
||||
<string name="account_common_error_server_message">Dychwelodd y gweinydd y negae ganlynol:\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Næste</string>
|
||||
<string name="account_common_button_back">Tilbage</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Weiter</string>
|
||||
<string name="account_common_button_back">Zurück</string>
|
||||
<string name="account_common_error_server_message">Der Server hat die folgende Nachricht zurückgegeben:\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Επόμενο</string>
|
||||
<string name="account_common_button_back">Πίσω</string>
|
||||
<string name="account_common_error_server_message">Ο διακομιστής επέστρεψε το ακόλουθο μήνυμα:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Next</string>
|
||||
<string name="account_common_button_back">Back</string>
|
||||
<string name="account_common_error_server_message">The server returned the following message:\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_error_server_message">La servilo sendis la jenan mesaĝon:
|
||||
\n%s</string>
|
||||
<string name="account_common_button_next">Sekven</string>
|
||||
<string name="account_common_button_back">Reen</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Siguiente</string>
|
||||
<string name="account_common_button_back">Atrás</string>
|
||||
<string name="account_common_error_server_message">El servidor devolvió el siguiente mensaje:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Edasi</string>
|
||||
<string name="account_common_button_back">Tagasi</string>
|
||||
<string name="account_common_error_server_message">Serveri vastuses sisaldus selline sõnum:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Hurrengoa</string>
|
||||
<string name="account_common_button_back">Aurrekoa</string>
|
||||
<string name="account_common_error_server_message">Zerbitzariak mezu hau itzuli du:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">بعدی</string>
|
||||
<string name="account_common_button_back">بازگشت</string>
|
||||
<string name="account_common_error_server_message">کارساز، پیام زیر را برگردانده است:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Seuraava</string>
|
||||
<string name="account_common_button_back">Takaisin</string>
|
||||
<string name="account_common_error_server_message">Palvelin palautti seuraavan viestin:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Suivant</string>
|
||||
<string name="account_common_button_back">Précédent</string>
|
||||
<string name="account_common_error_server_message">Le message suivant a été retourné par le serveur :\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Folgjende</string>
|
||||
<string name="account_common_button_back">Tebek</string>
|
||||
<string name="account_common_error_server_message">De server joech it folgjende berjocht werom:\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Ar aghaidh</string>
|
||||
<string name="account_common_button_back">Ar ais</string>
|
||||
<string name="account_common_error_server_message">Chuir an freastalaí an teachtaireacht seo a leanas ar ais:\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_error_server_message">Thill am frithealaiche an teachdaireachd a leanas:\n%s</string>
|
||||
<string name="account_common_button_next">Air adhart</string>
|
||||
<string name="account_common_button_back">Air ais</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">seguinte</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">अगला</string>
|
||||
<string name="account_common_button_back">पिछला</string>
|
||||
<string name="account_common_error_server_message">सेवक ने निम्न संदेश लौटायाः\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Sljedeće</string>
|
||||
<string name="account_common_button_back">Natrag</string>
|
||||
<string name="account_common_error_server_message">Poslužitelj je vratio sljedeću poruku:\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Következő</string>
|
||||
<string name="account_common_button_back">Vissza</string>
|
||||
<string name="account_common_error_server_message">A kiszolgáló a következő üzenetet küldte vissza:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_error_server_message">Peladen mengembalikan pesan berikut ini:\n%s</string>
|
||||
<string name="account_common_button_next">Berikutnya</string>
|
||||
<string name="account_common_button_back">Kembali</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Næsta</string>
|
||||
<string name="account_common_button_back">Til baka</string>
|
||||
<string name="account_common_error_server_message">Póstþjónninn svaraði með eftirfarandi skilaboðum:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Successivo</string>
|
||||
<string name="account_common_button_back">Precedente</string>
|
||||
<string name="account_common_error_server_message">Il server ha restituito il seguente messaggio:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_error_server_message">השרת החזיר את השגיאה הבאה:
|
||||
\n%s</string>
|
||||
<string name="account_common_button_next">הבא</string>
|
||||
<string name="account_common_button_back">הקודם</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">次へ</string>
|
||||
<string name="account_common_button_back">戻る</string>
|
||||
<string name="account_common_error_server_message">サーバーから以下のメッセージが返答されました:
|
||||
\n%s</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_common_button_next">Uḍfir</string>
|
||||
<string name="account_common_button_back">Uɣal</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue