Repo created
This commit is contained in:
parent
75dc487a7a
commit
39c29d175b
6317 changed files with 388324 additions and 2 deletions
34
feature/account/setup/build.gradle.kts
Normal file
34
feature/account/setup/build.gradle.kts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.androidCompose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.k9mail.feature.account.setup"
|
||||
resourcePrefix = "account_setup_"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.ui.compose.designsystem)
|
||||
implementation(projects.core.ui.compose.navigation)
|
||||
|
||||
implementation(projects.mail.common)
|
||||
implementation(projects.mail.protocols.imap)
|
||||
implementation(projects.mail.protocols.pop3)
|
||||
implementation(projects.mail.protocols.smtp)
|
||||
|
||||
implementation(projects.feature.autodiscovery.service)
|
||||
implementation(projects.feature.autodiscovery.demo)
|
||||
|
||||
api(projects.feature.account.common)
|
||||
implementation(projects.feature.account.oauth)
|
||||
implementation(projects.feature.account.server.settings)
|
||||
implementation(projects.feature.account.server.certificate)
|
||||
api(projects.feature.account.server.validation)
|
||||
|
||||
testImplementation(projects.core.logging.testing)
|
||||
testImplementation(projects.core.ui.compose.testing)
|
||||
|
||||
testImplementation(platform(libs.forkhandles.bom))
|
||||
testImplementation(libs.forkhandles.fabrikate4k)
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
|
||||
import app.k9mail.feature.account.common.domain.input.StringInputField
|
||||
import app.k9mail.feature.account.server.validation.ui.fake.FakeAccountOAuthViewModel
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.fake.fakeAutoDiscoveryResultSettings
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AccountAutoDiscoveryContentPreview() {
|
||||
PreviewWithTheme {
|
||||
AccountAutoDiscoveryContent(
|
||||
state = AccountAutoDiscoveryContract.State(),
|
||||
onEvent = {},
|
||||
oAuthViewModel = FakeAccountOAuthViewModel(),
|
||||
brandName = "BrandName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AccountAutoDiscoveryContentEmailPreview() {
|
||||
PreviewWithTheme {
|
||||
AccountAutoDiscoveryContent(
|
||||
state = AccountAutoDiscoveryContract.State(
|
||||
emailAddress = StringInputField(value = "test@example.com"),
|
||||
),
|
||||
onEvent = {},
|
||||
oAuthViewModel = FakeAccountOAuthViewModel(),
|
||||
brandName = "BrandName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AccountAutoDiscoveryContentPasswordPreview() {
|
||||
PreviewWithTheme {
|
||||
AccountAutoDiscoveryContent(
|
||||
state = AccountAutoDiscoveryContract.State(
|
||||
configStep = AccountAutoDiscoveryContract.ConfigStep.PASSWORD,
|
||||
emailAddress = StringInputField(value = "test@example.com"),
|
||||
autoDiscoverySettings = fakeAutoDiscoveryResultSettings(isTrusted = true),
|
||||
),
|
||||
onEvent = {},
|
||||
oAuthViewModel = FakeAccountOAuthViewModel(),
|
||||
brandName = "BrandName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AccountAutoDiscoveryContentPasswordUntrustedSettingsPreview() {
|
||||
PreviewWithTheme {
|
||||
AccountAutoDiscoveryContent(
|
||||
state = AccountAutoDiscoveryContract.State(
|
||||
configStep = AccountAutoDiscoveryContract.ConfigStep.PASSWORD,
|
||||
emailAddress = StringInputField(value = "test@example.com"),
|
||||
autoDiscoverySettings = fakeAutoDiscoveryResultSettings(isTrusted = false),
|
||||
),
|
||||
onEvent = {},
|
||||
oAuthViewModel = FakeAccountOAuthViewModel(),
|
||||
brandName = "BrandName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AccountAutoDiscoveryContentPasswordNoSettingsPreview() {
|
||||
PreviewWithTheme {
|
||||
AccountAutoDiscoveryContent(
|
||||
state = AccountAutoDiscoveryContract.State(
|
||||
configStep = AccountAutoDiscoveryContract.ConfigStep.PASSWORD,
|
||||
emailAddress = StringInputField(value = "test@example.com"),
|
||||
),
|
||||
onEvent = {},
|
||||
oAuthViewModel = FakeAccountOAuthViewModel(),
|
||||
brandName = "BrandName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AccountAutoDiscoveryContentOAuthPreview() {
|
||||
PreviewWithTheme {
|
||||
AccountAutoDiscoveryContent(
|
||||
state = AccountAutoDiscoveryContract.State(
|
||||
configStep = AccountAutoDiscoveryContract.ConfigStep.OAUTH,
|
||||
emailAddress = StringInputField(value = "test@example.com"),
|
||||
autoDiscoverySettings = fakeAutoDiscoveryResultSettings(isTrusted = true),
|
||||
),
|
||||
onEvent = {},
|
||||
oAuthViewModel = FakeAccountOAuthViewModel(),
|
||||
brandName = "BrandName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.core.ui.compose.common.annotation.PreviewDevicesWithBackground
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
|
||||
import app.k9mail.feature.account.common.ui.fake.FakeAccountStateRepository
|
||||
import app.k9mail.feature.account.server.validation.ui.fake.FakeAccountOAuthViewModel
|
||||
import app.k9mail.feature.account.setup.ui.fake.FakeBrandNameProvider
|
||||
|
||||
@Composable
|
||||
@PreviewDevicesWithBackground
|
||||
internal fun AccountAutoDiscoveryScreenPreview() {
|
||||
PreviewWithTheme {
|
||||
AccountAutoDiscoveryScreen(
|
||||
onNext = {},
|
||||
onBack = {},
|
||||
viewModel = AccountAutoDiscoveryViewModel(
|
||||
validator = AccountAutoDiscoveryValidator(),
|
||||
getAutoDiscovery = { AutoDiscoveryResult.NoUsableSettingsFound },
|
||||
accountStateRepository = FakeAccountStateRepository(),
|
||||
oAuthViewModel = FakeAccountOAuthViewModel(),
|
||||
),
|
||||
brandNameProvider = FakeBrandNameProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery.fake
|
||||
|
||||
import app.k9mail.autodiscovery.api.AuthenticationType
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.autodiscovery.api.ConnectionSecurity
|
||||
import app.k9mail.autodiscovery.api.ImapServerSettings
|
||||
import app.k9mail.autodiscovery.api.SmtpServerSettings
|
||||
import net.thunderbird.core.common.net.toHostname
|
||||
import net.thunderbird.core.common.net.toPort
|
||||
|
||||
internal fun fakeAutoDiscoveryResultSettings(isTrusted: Boolean) =
|
||||
AutoDiscoveryResult.Settings(
|
||||
incomingServerSettings = ImapServerSettings(
|
||||
hostname = "imap.example.com".toHostname(),
|
||||
port = 993.toPort(),
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
authenticationTypes = listOf(AuthenticationType.PasswordEncrypted),
|
||||
username = "",
|
||||
),
|
||||
outgoingServerSettings = SmtpServerSettings(
|
||||
hostname = "smtp.example.com".toHostname(),
|
||||
port = 465.toPort(),
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
authenticationTypes = listOf(AuthenticationType.PasswordEncrypted),
|
||||
username = "",
|
||||
),
|
||||
isTrusted = isTrusted,
|
||||
source = "preview",
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery.view
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
|
||||
import app.k9mail.feature.account.common.domain.input.BooleanInputField
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AutoDiscoveryResultApprovalViewPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryResultApprovalView(
|
||||
approvalState = BooleanInputField(
|
||||
value = true,
|
||||
isValid = true,
|
||||
),
|
||||
onApprovalChange = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery.view
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.fake.fakeAutoDiscoveryResultSettings
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AutoDiscoveryResultBodyViewPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryResultBodyView(
|
||||
settings = fakeAutoDiscoveryResultSettings(isTrusted = true),
|
||||
onEditConfigurationClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery.view
|
||||
|
||||
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 AutoDiscoveryResultHeaderViewTrustedCollapsedPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryResultHeaderView(
|
||||
state = AutoDiscoveryResultHeaderState.Trusted,
|
||||
isExpanded = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AutoDiscoveryResultHeaderViewTrustedExpandedPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryResultHeaderView(
|
||||
state = AutoDiscoveryResultHeaderState.Trusted,
|
||||
isExpanded = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AutoDiscoveryResultHeaderViewUntrustedCollapsedPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryResultHeaderView(
|
||||
state = AutoDiscoveryResultHeaderState.Untrusted,
|
||||
isExpanded = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AutoDiscoveryResultHeaderViewUntrustedExpandedPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryResultHeaderView(
|
||||
state = AutoDiscoveryResultHeaderState.Untrusted,
|
||||
isExpanded = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AutoDiscoveryResultHeaderNoSettingsPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryResultHeaderView(
|
||||
state = AutoDiscoveryResultHeaderState.NoSettings,
|
||||
isExpanded = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery.view
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.fake.fakeAutoDiscoveryResultSettings
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AutoDiscoveryResultViewTrustedPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryResultView(
|
||||
settings = fakeAutoDiscoveryResultSettings(isTrusted = true),
|
||||
onEditConfigurationClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AutoDiscoveryResultViewUntrustedPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryResultView(
|
||||
settings = fakeAutoDiscoveryResultSettings(isTrusted = false),
|
||||
onEditConfigurationClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery.view
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.autodiscovery.api.ConnectionSecurity
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
|
||||
import net.thunderbird.core.common.net.toHostname
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AutoDiscoveryServerSettingsViewPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryServerSettingsView(
|
||||
protocolName = "IMAP",
|
||||
serverHostname = "imap.example.com".toHostname(),
|
||||
serverPort = 993,
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AutoDiscoveryServerSettingsViewOutgoingPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryServerSettingsView(
|
||||
protocolName = "IMAP",
|
||||
serverHostname = "imap.example.com".toHostname(),
|
||||
serverPort = 993,
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
isIncoming = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AutoDiscoveryServerSettingsViewWithUserPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryServerSettingsView(
|
||||
protocolName = "IMAP",
|
||||
serverHostname = "imap.example.com".toHostname(),
|
||||
serverPort = 993,
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
username = "username",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun AutoDiscoveryServerSettingsViewWithIpAddressPreview() {
|
||||
PreviewWithThemes {
|
||||
AutoDiscoveryServerSettingsView(
|
||||
protocolName = "IMAP",
|
||||
serverHostname = "127.0.0.1".toHostname(),
|
||||
serverPort = 993,
|
||||
connectionSecurity = ConnectionSecurity.TLS,
|
||||
username = "username",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun CreateAccountContentSuccessPreview() {
|
||||
PreviewWithTheme {
|
||||
CreateAccountContent(
|
||||
state = CreateAccountContract.State(
|
||||
isLoading = false,
|
||||
error = null,
|
||||
),
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun CreateAccountContentLoadingPreview() {
|
||||
PreviewWithTheme {
|
||||
CreateAccountContent(
|
||||
state = CreateAccountContract.State(
|
||||
isLoading = true,
|
||||
error = null,
|
||||
),
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun CreateAccountContentErrorPreview() {
|
||||
PreviewWithTheme {
|
||||
CreateAccountContent(
|
||||
state = CreateAccountContract.State(
|
||||
isLoading = false,
|
||||
error = AccountCreatorResult.Error("Error message"),
|
||||
),
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import app.k9mail.core.ui.compose.common.annotation.PreviewDevices
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
|
||||
import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
import app.k9mail.feature.account.setup.ui.fake.FakeBrandNameProvider
|
||||
|
||||
@Composable
|
||||
@PreviewDevices
|
||||
internal fun AccountOptionsScreenK9Preview() {
|
||||
PreviewWithTheme {
|
||||
CreateAccountScreen(
|
||||
onNext = {},
|
||||
onBack = {},
|
||||
viewModel = CreateAccountViewModel(
|
||||
createAccount = { AccountCreatorResult.Success("irrelevant") },
|
||||
accountStateRepository = InMemoryAccountStateRepository(),
|
||||
),
|
||||
brandNameProvider = FakeBrandNameProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package app.k9mail.feature.account.setup.ui.fake
|
||||
|
||||
import net.thunderbird.core.common.provider.BrandNameProvider
|
||||
|
||||
internal object FakeBrandNameProvider : BrandNameProvider {
|
||||
override val brandName: String = "Fake Brand Name"
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.display
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun DisplayOptionsContentPreview() {
|
||||
PreviewWithTheme {
|
||||
DisplayOptionsContent(
|
||||
state = DisplayOptionsContract.State(),
|
||||
onEvent = {},
|
||||
contentPadding = PaddingValues(),
|
||||
brandName = "BrandName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.display
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
|
||||
import app.k9mail.feature.account.common.ui.fake.FakeAccountStateRepository
|
||||
import app.k9mail.feature.account.setup.ui.fake.FakeBrandNameProvider
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun DisplayOptionsScreenPreview() {
|
||||
PreviewWithTheme {
|
||||
DisplayOptionsScreen(
|
||||
onNext = {},
|
||||
onBack = {},
|
||||
viewModel = DisplayOptionsViewModel(
|
||||
validator = DisplayOptionsValidator(),
|
||||
accountStateRepository = FakeAccountStateRepository(),
|
||||
accountOwnerNameProvider = { null },
|
||||
),
|
||||
brandNameProvider = FakeBrandNameProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.sync
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun SyncOptionsContentPreview() {
|
||||
PreviewWithTheme {
|
||||
SyncOptionsContent(
|
||||
state = SyncOptionsContract.State(),
|
||||
onEvent = {},
|
||||
contentPadding = PaddingValues(),
|
||||
brandName = "BrandName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.sync
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
|
||||
import app.k9mail.feature.account.common.ui.fake.FakeAccountStateRepository
|
||||
import app.k9mail.feature.account.setup.ui.fake.FakeBrandNameProvider
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun SyncOptionsScreenPreview() {
|
||||
PreviewWithTheme {
|
||||
SyncOptionsScreen(
|
||||
onNext = {},
|
||||
onBack = {},
|
||||
viewModel = SyncOptionsViewModel(
|
||||
accountStateRepository = FakeAccountStateRepository(),
|
||||
),
|
||||
brandNameProvider = FakeBrandNameProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package app.k9mail.feature.account.setup.ui.specialfolders
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun SpecialFoldersContentLoadingPreview() {
|
||||
PreviewWithTheme {
|
||||
SpecialFoldersContent(
|
||||
state = SpecialFoldersContract.State(
|
||||
isLoading = true,
|
||||
),
|
||||
onEvent = {},
|
||||
contentPadding = PaddingValues(),
|
||||
brandName = "BrandName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun SpecialFoldersContentFormPreview() {
|
||||
PreviewWithTheme {
|
||||
SpecialFoldersContent(
|
||||
state = SpecialFoldersContract.State(
|
||||
isLoading = false,
|
||||
),
|
||||
onEvent = {},
|
||||
contentPadding = PaddingValues(),
|
||||
brandName = "BrandName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun SpecialFoldersContentSuccessPreview() {
|
||||
PreviewWithTheme {
|
||||
SpecialFoldersContent(
|
||||
state = SpecialFoldersContract.State(
|
||||
isLoading = false,
|
||||
isSuccess = true,
|
||||
),
|
||||
onEvent = {},
|
||||
contentPadding = PaddingValues(),
|
||||
brandName = "BrandName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun SpecialFoldersContentErrorPreview() {
|
||||
PreviewWithTheme {
|
||||
SpecialFoldersContent(
|
||||
state = SpecialFoldersContract.State(
|
||||
isLoading = false,
|
||||
error = SpecialFoldersContract.Failure.LoadFoldersFailed("Error"),
|
||||
),
|
||||
onEvent = {},
|
||||
contentPadding = PaddingValues(),
|
||||
brandName = "BrandName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package app.k9mail.feature.account.setup.ui.specialfolders
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun SpecialFoldersFormContentPreview() {
|
||||
PreviewWithTheme {
|
||||
SpecialFoldersFormContent(
|
||||
state = SpecialFoldersContract.FormState(),
|
||||
onEvent = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package app.k9mail.feature.account.setup.ui.specialfolders
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import app.k9mail.core.ui.compose.common.annotation.PreviewDevices
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
|
||||
import app.k9mail.feature.account.setup.ui.fake.FakeBrandNameProvider
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.fake.FakeSpecialFoldersViewModel
|
||||
|
||||
@Composable
|
||||
@PreviewDevices
|
||||
internal fun SpecialFoldersScreenPreview() {
|
||||
PreviewWithTheme {
|
||||
SpecialFoldersScreen(
|
||||
onNext = {},
|
||||
onBack = {},
|
||||
viewModel = FakeSpecialFoldersViewModel(),
|
||||
brandNameProvider = FakeBrandNameProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package app.k9mail.feature.account.setup.ui.specialfolders.fake
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.State
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.ViewModel
|
||||
|
||||
class FakeSpecialFoldersViewModel(
|
||||
initialState: State = State(),
|
||||
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
|
||||
|
||||
val events = mutableListOf<Event>()
|
||||
|
||||
override fun event(event: Event) {
|
||||
events.add(event)
|
||||
}
|
||||
|
||||
fun effect(effect: Effect) {
|
||||
emitEffect(effect)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package app.k9mail.feature.account.setup
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.Account
|
||||
|
||||
interface AccountSetupExternalContract {
|
||||
|
||||
fun interface AccountCreator {
|
||||
suspend fun createAccount(account: Account): AccountCreatorResult
|
||||
|
||||
sealed interface AccountCreatorResult {
|
||||
data class Success(val accountUuid: String) : AccountCreatorResult
|
||||
data class Error(val message: String) : AccountCreatorResult
|
||||
}
|
||||
}
|
||||
|
||||
fun interface AccountOwnerNameProvider {
|
||||
suspend fun getOwnerName(): String?
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package app.k9mail.feature.account.setup
|
||||
|
||||
import app.k9mail.autodiscovery.api.AutoDiscovery
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryRegistry
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryService
|
||||
import app.k9mail.autodiscovery.service.RealAutoDiscoveryRegistry
|
||||
import app.k9mail.autodiscovery.service.RealAutoDiscoveryService
|
||||
import app.k9mail.feature.account.common.featureAccountCommonModule
|
||||
import app.k9mail.feature.account.oauth.featureAccountOAuthModule
|
||||
import app.k9mail.feature.account.server.settings.featureAccountServerSettingsModule
|
||||
import app.k9mail.feature.account.server.validation.featureAccountServerValidationModule
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract
|
||||
import app.k9mail.feature.account.setup.domain.usecase.CreateAccount
|
||||
import app.k9mail.feature.account.setup.domain.usecase.GetAutoDiscovery
|
||||
import app.k9mail.feature.account.setup.domain.usecase.GetSpecialFolderOptions
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateSpecialFolderOptions
|
||||
import app.k9mail.feature.account.setup.navigation.AccountSetupNavigation
|
||||
import app.k9mail.feature.account.setup.navigation.DefaultAccountSetupNavigation
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryValidator
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryViewModel
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountViewModel
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsValidator
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsViewModel
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsViewModel
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersFormUiModel
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersViewModel
|
||||
import com.fsck.k9.mail.folders.FolderFetcher
|
||||
import com.fsck.k9.mail.store.imap.ImapFolderFetcher
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.module.dsl.viewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val featureAccountSetupModule: Module = module {
|
||||
includes(
|
||||
featureAccountCommonModule,
|
||||
featureAccountOAuthModule,
|
||||
featureAccountServerValidationModule,
|
||||
featureAccountServerSettingsModule,
|
||||
)
|
||||
|
||||
single<AccountSetupNavigation> { DefaultAccountSetupNavigation() }
|
||||
|
||||
single<OkHttpClient> {
|
||||
OkHttpClient()
|
||||
}
|
||||
|
||||
single<AutoDiscoveryRegistry> {
|
||||
val extraAutoDiscoveries = get<List<AutoDiscovery>>(named("extraAutoDiscoveries"))
|
||||
RealAutoDiscoveryRegistry(
|
||||
autoDiscoveries = RealAutoDiscoveryRegistry.createDefaultAutoDiscoveries(
|
||||
okHttpClient = get(),
|
||||
) + extraAutoDiscoveries,
|
||||
)
|
||||
}
|
||||
|
||||
single<AutoDiscoveryService> {
|
||||
RealAutoDiscoveryService(
|
||||
autoDiscoveryRegistry = get(),
|
||||
)
|
||||
}
|
||||
|
||||
single<DomainContract.UseCase.GetAutoDiscovery> {
|
||||
GetAutoDiscovery(
|
||||
service = get(),
|
||||
oauthProvider = get(),
|
||||
)
|
||||
}
|
||||
|
||||
factory<DomainContract.UseCase.CreateAccount> {
|
||||
CreateAccount(
|
||||
accountCreator = get(),
|
||||
)
|
||||
}
|
||||
|
||||
factory<AccountAutoDiscoveryContract.Validator> { AccountAutoDiscoveryValidator() }
|
||||
factory<DisplayOptionsContract.Validator> { DisplayOptionsValidator() }
|
||||
|
||||
viewModel {
|
||||
AccountAutoDiscoveryViewModel(
|
||||
validator = get(),
|
||||
getAutoDiscovery = get(),
|
||||
accountStateRepository = get(),
|
||||
oAuthViewModel = get(),
|
||||
)
|
||||
}
|
||||
|
||||
factory<FolderFetcher> {
|
||||
ImapFolderFetcher(
|
||||
trustedSocketFactory = get(),
|
||||
oAuth2TokenProviderFactory = get(),
|
||||
clientInfoAppName = get(named("ClientInfoAppName")),
|
||||
clientInfoAppVersion = get(named("ClientInfoAppVersion")),
|
||||
)
|
||||
}
|
||||
|
||||
factory<DomainContract.UseCase.GetSpecialFolderOptions> {
|
||||
GetSpecialFolderOptions(
|
||||
folderFetcher = get(),
|
||||
accountStateRepository = get(),
|
||||
authStateStorage = get(),
|
||||
)
|
||||
}
|
||||
|
||||
factory<DomainContract.UseCase.ValidateSpecialFolderOptions> {
|
||||
ValidateSpecialFolderOptions()
|
||||
}
|
||||
|
||||
factory<SpecialFoldersContract.FormUiModel> {
|
||||
SpecialFoldersFormUiModel()
|
||||
}
|
||||
|
||||
viewModel {
|
||||
SpecialFoldersViewModel(
|
||||
formUiModel = get(),
|
||||
getSpecialFolderOptions = get(),
|
||||
validateSpecialFolderOptions = get(),
|
||||
accountStateRepository = get(),
|
||||
)
|
||||
}
|
||||
|
||||
viewModel {
|
||||
DisplayOptionsViewModel(
|
||||
validator = get(),
|
||||
accountStateRepository = get(),
|
||||
accountOwnerNameProvider = get(),
|
||||
)
|
||||
}
|
||||
|
||||
viewModel {
|
||||
SyncOptionsViewModel(
|
||||
accountStateRepository = get(),
|
||||
)
|
||||
}
|
||||
|
||||
viewModel {
|
||||
CreateAccountViewModel(
|
||||
createAccount = get(),
|
||||
accountStateRepository = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package app.k9mail.feature.account.setup.domain
|
||||
|
||||
import app.k9mail.autodiscovery.api.ImapServerSettings
|
||||
import app.k9mail.autodiscovery.api.IncomingServerSettings
|
||||
import app.k9mail.autodiscovery.api.OutgoingServerSettings
|
||||
import app.k9mail.autodiscovery.api.SmtpServerSettings
|
||||
import app.k9mail.autodiscovery.demo.DemoServerSettings
|
||||
import app.k9mail.feature.account.common.domain.entity.toAuthType
|
||||
import app.k9mail.feature.account.common.domain.entity.toMailConnectionSecurity
|
||||
import app.k9mail.feature.account.setup.domain.entity.toAuthenticationType
|
||||
import app.k9mail.feature.account.setup.domain.entity.toConnectionSecurity
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreSettings
|
||||
|
||||
internal fun IncomingServerSettings.toServerSettings(password: String?): ServerSettings {
|
||||
return when (this) {
|
||||
is ImapServerSettings -> this.toImapServerSettings(password)
|
||||
is DemoServerSettings -> this.serverSettings
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown server settings type: $this")
|
||||
}
|
||||
}
|
||||
|
||||
private fun ImapServerSettings.toImapServerSettings(password: String?): ServerSettings {
|
||||
return ServerSettings(
|
||||
type = "imap",
|
||||
host = hostname.value,
|
||||
port = port.value,
|
||||
connectionSecurity = connectionSecurity.toConnectionSecurity().toMailConnectionSecurity(),
|
||||
authenticationType = authenticationTypes.first().toAuthenticationType().toAuthType(),
|
||||
username = username,
|
||||
password = password,
|
||||
clientCertificateAlias = null,
|
||||
extra = ImapStoreSettings.createExtra(
|
||||
autoDetectNamespace = true,
|
||||
pathPrefix = null,
|
||||
useCompression = true,
|
||||
sendClientInfo = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert [OutgoingServerSettings] to [ServerSettings].
|
||||
*
|
||||
* @throws IllegalArgumentException if the server settings type is unknown.
|
||||
*/
|
||||
internal fun OutgoingServerSettings.toServerSettings(password: String?): ServerSettings {
|
||||
return when (this) {
|
||||
is SmtpServerSettings -> this.toSmtpServerSettings(password)
|
||||
is DemoServerSettings -> this.serverSettings
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown server settings type: $this")
|
||||
}
|
||||
}
|
||||
|
||||
private fun SmtpServerSettings.toSmtpServerSettings(password: String?): ServerSettings {
|
||||
return ServerSettings(
|
||||
type = "smtp",
|
||||
host = hostname.value,
|
||||
port = port.value,
|
||||
connectionSecurity = connectionSecurity.toConnectionSecurity().toMailConnectionSecurity(),
|
||||
authenticationType = authenticationTypes.first().toAuthenticationType().toAuthType(),
|
||||
username = username,
|
||||
password = password,
|
||||
clientCertificateAlias = null,
|
||||
extra = emptyMap(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package app.k9mail.feature.account.setup.domain
|
||||
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountState
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderOptions
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationError
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
interface DomainContract {
|
||||
|
||||
interface UseCase {
|
||||
fun interface GetAutoDiscovery {
|
||||
suspend fun execute(emailAddress: String): AutoDiscoveryResult
|
||||
}
|
||||
|
||||
fun interface CreateAccount {
|
||||
suspend fun execute(accountState: AccountState): AccountCreatorResult
|
||||
}
|
||||
|
||||
fun interface ValidateEmailAddress {
|
||||
fun execute(emailAddress: String): ValidationResult
|
||||
}
|
||||
|
||||
fun interface ValidateConfigurationApproval {
|
||||
fun execute(isApproved: Boolean?, isAutoDiscoveryTrusted: Boolean?): ValidationResult
|
||||
}
|
||||
|
||||
fun interface ValidateAccountName {
|
||||
fun execute(accountName: String): ValidationResult
|
||||
}
|
||||
|
||||
fun interface ValidateDisplayName {
|
||||
fun execute(displayName: String): ValidationResult
|
||||
}
|
||||
|
||||
fun interface ValidateEmailSignature {
|
||||
fun execute(emailSignature: String): ValidationResult
|
||||
}
|
||||
|
||||
fun interface GetSpecialFolderOptions {
|
||||
suspend operator fun invoke(): SpecialFolderOptions
|
||||
}
|
||||
|
||||
fun interface ValidateSpecialFolderOptions {
|
||||
operator fun invoke(specialFolderOptions: SpecialFolderOptions): ValidationResult
|
||||
|
||||
sealed interface Failure : ValidationError {
|
||||
data object MissingDefaultSpecialFolderOption : Failure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package app.k9mail.feature.account.setup.domain.entity
|
||||
|
||||
@JvmInline
|
||||
value class AccountUuid(val value: String)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package app.k9mail.feature.account.setup.domain.entity
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.AuthenticationType
|
||||
|
||||
typealias AutoDiscoveryAuthenticationType = app.k9mail.autodiscovery.api.AuthenticationType
|
||||
|
||||
internal fun AutoDiscoveryAuthenticationType.toAuthenticationType(): AuthenticationType {
|
||||
return when (this) {
|
||||
AutoDiscoveryAuthenticationType.PasswordCleartext -> AuthenticationType.PasswordCleartext
|
||||
AutoDiscoveryAuthenticationType.PasswordEncrypted -> AuthenticationType.PasswordEncrypted
|
||||
AutoDiscoveryAuthenticationType.OAuth2 -> AuthenticationType.OAuth2
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package app.k9mail.feature.account.setup.domain.entity
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.ConnectionSecurity
|
||||
|
||||
internal typealias AutoDiscoveryConnectionSecurity = app.k9mail.autodiscovery.api.ConnectionSecurity
|
||||
|
||||
internal fun AutoDiscoveryConnectionSecurity.toConnectionSecurity(): ConnectionSecurity {
|
||||
return when (this) {
|
||||
AutoDiscoveryConnectionSecurity.StartTLS -> ConnectionSecurity.StartTLS
|
||||
AutoDiscoveryConnectionSecurity.TLS -> ConnectionSecurity.TLS
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package app.k9mail.feature.account.setup.domain.entity
|
||||
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
enum class EmailCheckFrequency(
|
||||
val minutes: Int,
|
||||
) {
|
||||
MANUAL(-1),
|
||||
EVERY_15_MINUTES(15),
|
||||
EVERY_30_MINUTES(30),
|
||||
EVERY_HOUR(1.fromHour()),
|
||||
EVERY_2_HOURS(2.fromHour()),
|
||||
EVERY_3_HOURS(3.fromHour()),
|
||||
EVERY_6_HOURS(6.fromHour()),
|
||||
EVERY_12_HOURS(12.fromHour()),
|
||||
EVERY_24_HOURS(24.fromHour()),
|
||||
;
|
||||
|
||||
companion object {
|
||||
val DEFAULT = EVERY_HOUR
|
||||
fun all() = entries.toImmutableList()
|
||||
|
||||
fun fromMinutes(minutes: Int): EmailCheckFrequency {
|
||||
return all().find { it.minutes == minutes } ?: DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun Int.fromHour(): Int {
|
||||
return 60 * this
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package app.k9mail.feature.account.setup.domain.entity
|
||||
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
enum class EmailDisplayCount(
|
||||
val count: Int,
|
||||
) {
|
||||
MESSAGES_10(10),
|
||||
MESSAGES_25(25),
|
||||
MESSAGES_50(50),
|
||||
MESSAGES_100(100),
|
||||
MESSAGES_250(250),
|
||||
MESSAGES_500(500),
|
||||
MESSAGES_1000(1000),
|
||||
;
|
||||
|
||||
companion object {
|
||||
val DEFAULT = MESSAGES_100
|
||||
fun all() = entries.toImmutableList()
|
||||
|
||||
fun fromCount(count: Int): EmailDisplayCount {
|
||||
return all().find { it.count == count } ?: DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package app.k9mail.feature.account.setup.domain.entity
|
||||
|
||||
import app.k9mail.autodiscovery.api.ImapServerSettings
|
||||
import app.k9mail.autodiscovery.api.IncomingServerSettings
|
||||
import app.k9mail.feature.account.common.domain.entity.IncomingProtocolType
|
||||
|
||||
internal fun IncomingServerSettings.toIncomingProtocolType(): IncomingProtocolType {
|
||||
when (this) {
|
||||
is ImapServerSettings -> return IncomingProtocolType.IMAP
|
||||
else -> throw IllegalArgumentException("Unsupported incoming server settings type: $this")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package app.k9mail.feature.account.setup.domain.usecase
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.Account
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountDisplayOptions
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountOptions
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountState
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountSyncOptions
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase
|
||||
import java.util.UUID
|
||||
|
||||
class CreateAccount(
|
||||
private val accountCreator: AccountCreator,
|
||||
private val uuidGenerator: () -> String = { UUID.randomUUID().toString() },
|
||||
) : UseCase.CreateAccount {
|
||||
override suspend fun execute(accountState: AccountState): AccountCreatorResult {
|
||||
val account = Account(
|
||||
uuid = uuidGenerator(),
|
||||
emailAddress = accountState.emailAddress!!,
|
||||
incomingServerSettings = accountState.incomingServerSettings!!.copy(),
|
||||
outgoingServerSettings = accountState.outgoingServerSettings!!.copy(),
|
||||
authorizationState = accountState.authorizationState?.value,
|
||||
specialFolderSettings = accountState.specialFolderSettings,
|
||||
options = mapOptions(accountState.displayOptions!!, accountState.syncOptions!!),
|
||||
)
|
||||
|
||||
return accountCreator.createAccount(account)
|
||||
}
|
||||
|
||||
private fun mapOptions(
|
||||
displayOptions: AccountDisplayOptions,
|
||||
syncOptions: AccountSyncOptions,
|
||||
): AccountOptions {
|
||||
return AccountOptions(
|
||||
accountName = displayOptions.accountName,
|
||||
displayName = displayOptions.displayName,
|
||||
emailSignature = displayOptions.emailSignature,
|
||||
checkFrequencyInMinutes = syncOptions.checkFrequencyInMinutes,
|
||||
messageDisplayCount = syncOptions.messageDisplayCount,
|
||||
showNotification = syncOptions.showNotification,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package app.k9mail.feature.account.setup.domain.usecase
|
||||
|
||||
import app.k9mail.autodiscovery.api.AuthenticationType
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryService
|
||||
import app.k9mail.autodiscovery.api.ImapServerSettings
|
||||
import app.k9mail.autodiscovery.api.SmtpServerSettings
|
||||
import app.k9mail.autodiscovery.demo.DemoServerSettings
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract
|
||||
import net.thunderbird.core.common.mail.toUserEmailAddress
|
||||
import net.thunderbird.core.common.oauth.OAuthConfigurationProvider
|
||||
|
||||
internal class GetAutoDiscovery(
|
||||
private val service: AutoDiscoveryService,
|
||||
private val oauthProvider: OAuthConfigurationProvider,
|
||||
) : DomainContract.UseCase.GetAutoDiscovery {
|
||||
override suspend fun execute(emailAddress: String): AutoDiscoveryResult {
|
||||
val email = emailAddress.toUserEmailAddress()
|
||||
|
||||
val result = service.discover(email)
|
||||
|
||||
return if (result is AutoDiscoveryResult.Settings) {
|
||||
if (result.incomingServerSettings is DemoServerSettings) {
|
||||
return result
|
||||
} else {
|
||||
validateOAuthSupport(result)
|
||||
}
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateOAuthSupport(settings: AutoDiscoveryResult.Settings): AutoDiscoveryResult {
|
||||
if (settings.incomingServerSettings !is ImapServerSettings ||
|
||||
settings.outgoingServerSettings !is SmtpServerSettings
|
||||
) {
|
||||
return AutoDiscoveryResult.NoUsableSettingsFound
|
||||
}
|
||||
|
||||
val incomingServerSettings = settings.incomingServerSettings as ImapServerSettings
|
||||
val outgoingServerSettings = settings.outgoingServerSettings as SmtpServerSettings
|
||||
|
||||
val incomingAuthenticationTypes = cleanAuthenticationTypes(
|
||||
authenticationTypes = incomingServerSettings.authenticationTypes,
|
||||
hostname = incomingServerSettings.hostname.value,
|
||||
)
|
||||
val outgoingAuthenticationTypes = cleanAuthenticationTypes(
|
||||
authenticationTypes = outgoingServerSettings.authenticationTypes,
|
||||
hostname = outgoingServerSettings.hostname.value,
|
||||
)
|
||||
|
||||
return if (incomingAuthenticationTypes.isNotEmpty() && outgoingAuthenticationTypes.isNotEmpty()) {
|
||||
settings.copy(
|
||||
incomingServerSettings = incomingServerSettings.copy(
|
||||
authenticationTypes = incomingAuthenticationTypes,
|
||||
),
|
||||
outgoingServerSettings = outgoingServerSettings.copy(
|
||||
authenticationTypes = outgoingAuthenticationTypes,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
AutoDiscoveryResult.NoUsableSettingsFound
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanAuthenticationTypes(
|
||||
authenticationTypes: List<AuthenticationType>,
|
||||
hostname: String,
|
||||
): List<AuthenticationType> {
|
||||
return if (AuthenticationType.OAuth2 in authenticationTypes && !isOAuthSupportedFor(hostname)) {
|
||||
// OAuth2 is not supported for this hostname; remove it from the list of supported authentication types
|
||||
authenticationTypes.filter { it != AuthenticationType.OAuth2 }
|
||||
} else {
|
||||
authenticationTypes
|
||||
}
|
||||
}
|
||||
|
||||
private fun isOAuthSupportedFor(hostname: String): Boolean {
|
||||
return oauthProvider.getConfiguration(hostname) != null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package app.k9mail.feature.account.setup.domain.usecase
|
||||
|
||||
import app.k9mail.feature.account.common.domain.AccountDomainContract
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderOption
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderOptions
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase
|
||||
import com.fsck.k9.mail.FolderType
|
||||
import com.fsck.k9.mail.folders.FolderFetcher
|
||||
import com.fsck.k9.mail.folders.RemoteFolder
|
||||
import com.fsck.k9.mail.oauth.AuthStateStorage
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class GetSpecialFolderOptions(
|
||||
private val folderFetcher: FolderFetcher,
|
||||
private val accountStateRepository: AccountDomainContract.AccountStateRepository,
|
||||
private val authStateStorage: AuthStateStorage,
|
||||
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) : UseCase.GetSpecialFolderOptions {
|
||||
override suspend fun invoke(): SpecialFolderOptions {
|
||||
return withContext(coroutineDispatcher) {
|
||||
val serverSettings = accountStateRepository.getState().incomingServerSettings
|
||||
?: error("No incoming server settings available")
|
||||
|
||||
val remoteFolders = folderFetcher.getFolders(serverSettings, authStateStorage)
|
||||
.sortedWith(
|
||||
compareByDescending<RemoteFolder> { it.type == FolderType.INBOX }
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.displayName },
|
||||
)
|
||||
|
||||
SpecialFolderOptions(
|
||||
archiveSpecialFolderOptions = mapByFolderType(FolderType.ARCHIVE, remoteFolders),
|
||||
draftsSpecialFolderOptions = mapByFolderType(FolderType.DRAFTS, remoteFolders),
|
||||
sentSpecialFolderOptions = mapByFolderType(FolderType.SENT, remoteFolders),
|
||||
spamSpecialFolderOptions = mapByFolderType(FolderType.SPAM, remoteFolders),
|
||||
trashSpecialFolderOptions = mapByFolderType(FolderType.TRASH, remoteFolders),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapByFolderType(
|
||||
folderType: FolderType,
|
||||
remoteFolders: List<RemoteFolder>,
|
||||
): List<SpecialFolderOption> {
|
||||
val automaticFolder = selectAutomaticFolderByType(folderType, remoteFolders)
|
||||
val folders = remoteFolders.map { remoteFolder ->
|
||||
getFolderByType(remoteFolder)
|
||||
}
|
||||
|
||||
return (listOf(automaticFolder, SpecialFolderOption.None()) + folders)
|
||||
}
|
||||
|
||||
// This uses the same implementation as the SpecialFolderSelectionStrategy. In case the implementation of the
|
||||
// SpecialFolderSelectionStrategy changes, this use case should be updated accordingly.
|
||||
private fun selectAutomaticFolderByType(
|
||||
folderType: FolderType,
|
||||
remoteFolders: List<RemoteFolder>,
|
||||
): SpecialFolderOption = remoteFolders.firstOrNull { folder -> folder.type == folderType }
|
||||
?.let {
|
||||
getFolderByType(
|
||||
remoteFolder = it,
|
||||
isAutomatic = true,
|
||||
)
|
||||
} ?: SpecialFolderOption.None(isAutomatic = true)
|
||||
|
||||
private fun getFolderByType(
|
||||
remoteFolder: RemoteFolder,
|
||||
isAutomatic: Boolean = false,
|
||||
): SpecialFolderOption {
|
||||
return when (remoteFolder.type) {
|
||||
FolderType.INBOX,
|
||||
FolderType.OUTBOX,
|
||||
FolderType.ARCHIVE,
|
||||
FolderType.DRAFTS,
|
||||
FolderType.SENT,
|
||||
FolderType.SPAM,
|
||||
FolderType.TRASH,
|
||||
-> SpecialFolderOption.Special(isAutomatic, remoteFolder)
|
||||
|
||||
FolderType.REGULAR -> SpecialFolderOption.Regular(remoteFolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package app.k9mail.feature.account.setup.domain.usecase
|
||||
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationError
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
internal class ValidateAccountName : DomainContract.UseCase.ValidateAccountName {
|
||||
override fun execute(accountName: String): ValidationResult {
|
||||
return when {
|
||||
accountName.isEmpty() -> ValidationResult.Success
|
||||
accountName.isBlank() -> ValidationResult.Failure(ValidateAccountNameError.BlankAccountName)
|
||||
else -> ValidationResult.Success
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ValidateAccountNameError : ValidationError {
|
||||
data object BlankAccountName : ValidateAccountNameError
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package app.k9mail.feature.account.setup.domain.usecase
|
||||
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationError
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
class ValidateConfigurationApproval : UseCase.ValidateConfigurationApproval {
|
||||
override fun execute(isApproved: Boolean?, isAutoDiscoveryTrusted: Boolean?): ValidationResult {
|
||||
return if (isApproved == null && isAutoDiscoveryTrusted == null) {
|
||||
ValidationResult.Success
|
||||
} else if (isAutoDiscoveryTrusted == true) {
|
||||
ValidationResult.Success
|
||||
} else if (isApproved == true) {
|
||||
ValidationResult.Success
|
||||
} else {
|
||||
ValidationResult.Failure(ValidateConfigurationApprovalError.ApprovalRequired)
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ValidateConfigurationApprovalError : ValidationError {
|
||||
data object ApprovalRequired : ValidateConfigurationApprovalError
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package app.k9mail.feature.account.setup.domain.usecase
|
||||
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationError
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
internal class ValidateDisplayName : DomainContract.UseCase.ValidateDisplayName {
|
||||
|
||||
override fun execute(displayName: String): ValidationResult {
|
||||
return when {
|
||||
displayName.isBlank() -> ValidationResult.Failure(ValidateDisplayNameError.EmptyDisplayName)
|
||||
else -> ValidationResult.Success
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ValidateDisplayNameError : ValidationError {
|
||||
data object EmptyDisplayName : ValidateDisplayNameError
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package app.k9mail.feature.account.setup.domain.usecase
|
||||
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationError
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
import net.thunderbird.core.common.mail.EmailAddressParserError
|
||||
import net.thunderbird.core.common.mail.EmailAddressParserException
|
||||
import net.thunderbird.core.common.mail.toEmailAddressOrNull
|
||||
import net.thunderbird.core.common.mail.toUserEmailAddress
|
||||
import net.thunderbird.core.logging.legacy.Log
|
||||
|
||||
/**
|
||||
* Validate an email address that the user wants to add to an account.
|
||||
*
|
||||
* This only allows a subset of all valid email addresses. We currently don't support international email addresses
|
||||
* and don't allow quoted local parts, or email addresses exceeding length restrictions.
|
||||
*
|
||||
* Note: Do NOT use this to validate recipients in incoming or outgoing messages. Use [String.toEmailAddressOrNull]
|
||||
* instead.
|
||||
*/
|
||||
class ValidateEmailAddress : UseCase.ValidateEmailAddress {
|
||||
|
||||
override fun execute(emailAddress: String): ValidationResult {
|
||||
if (emailAddress.isBlank()) {
|
||||
return ValidationResult.Failure(ValidateEmailAddressError.EmptyEmailAddress)
|
||||
}
|
||||
|
||||
return try {
|
||||
val parsedEmailAddress = emailAddress.toUserEmailAddress()
|
||||
|
||||
if (parsedEmailAddress.warnings.isEmpty()) {
|
||||
ValidationResult.Success
|
||||
} else {
|
||||
ValidationResult.Failure(ValidateEmailAddressError.NotAllowed)
|
||||
}
|
||||
} catch (e: EmailAddressParserException) {
|
||||
Log.v(e, "Error parsing email address: %s", emailAddress)
|
||||
|
||||
val validationError = when (e.error) {
|
||||
EmailAddressParserError.AddressLiteralsNotSupported,
|
||||
EmailAddressParserError.LocalPartLengthExceeded,
|
||||
EmailAddressParserError.DnsLabelLengthExceeded,
|
||||
EmailAddressParserError.DomainLengthExceeded,
|
||||
EmailAddressParserError.TotalLengthExceeded,
|
||||
EmailAddressParserError.QuotedStringInLocalPart,
|
||||
EmailAddressParserError.LocalPartRequiresQuotedString,
|
||||
EmailAddressParserError.EmptyLocalPart,
|
||||
-> {
|
||||
ValidateEmailAddressError.NotAllowed
|
||||
}
|
||||
|
||||
else -> {
|
||||
if ('@' in emailAddress) {
|
||||
// We currently don't support or recognize international email addresses. So if the string
|
||||
// contains an "@" character, we assume it's a valid email address that we don't support.
|
||||
ValidateEmailAddressError.InvalidOrNotSupported
|
||||
} else {
|
||||
ValidateEmailAddressError.InvalidEmailAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ValidationResult.Failure(validationError)
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ValidateEmailAddressError : ValidationError {
|
||||
data object EmptyEmailAddress : ValidateEmailAddressError
|
||||
data object NotAllowed : ValidateEmailAddressError
|
||||
data object InvalidOrNotSupported : ValidateEmailAddressError
|
||||
data object InvalidEmailAddress : ValidateEmailAddressError
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package app.k9mail.feature.account.setup.domain.usecase
|
||||
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailSignature.ValidateEmailSignatureError.BlankEmailSignature
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationError
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
// TODO check signature for input validity
|
||||
internal class ValidateEmailSignature : DomainContract.UseCase.ValidateEmailSignature {
|
||||
|
||||
override fun execute(emailSignature: String): ValidationResult {
|
||||
return when {
|
||||
emailSignature.isEmpty() -> ValidationResult.Success
|
||||
emailSignature.isBlank() -> ValidationResult.Failure(error = BlankEmailSignature)
|
||||
else -> ValidationResult.Success
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ValidateEmailSignatureError : ValidationError {
|
||||
data object BlankEmailSignature : ValidateEmailSignatureError
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package app.k9mail.feature.account.setup.domain.usecase
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderOption
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderOptions
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase.ValidateSpecialFolderOptions.Failure
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
class ValidateSpecialFolderOptions : UseCase.ValidateSpecialFolderOptions {
|
||||
override fun invoke(specialFolderOptions: SpecialFolderOptions): ValidationResult {
|
||||
return if (specialFolderOptions.hasMissingDefaultOption()) {
|
||||
ValidationResult.Failure(error = Failure.MissingDefaultSpecialFolderOption)
|
||||
} else {
|
||||
ValidationResult.Success
|
||||
}
|
||||
}
|
||||
|
||||
private fun SpecialFolderOptions.hasMissingDefaultOption(): Boolean {
|
||||
return archiveSpecialFolderOptions.hasMissingDefaultFolder() ||
|
||||
draftsSpecialFolderOptions.hasMissingDefaultFolder() ||
|
||||
sentSpecialFolderOptions.hasMissingDefaultFolder() ||
|
||||
spamSpecialFolderOptions.hasMissingDefaultFolder() ||
|
||||
trashSpecialFolderOptions.hasMissingDefaultFolder()
|
||||
}
|
||||
|
||||
private fun List<SpecialFolderOption>.hasMissingDefaultFolder(): Boolean {
|
||||
return first() is SpecialFolderOption.None
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
package app.k9mail.feature.account.setup.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import app.k9mail.feature.account.common.domain.entity.IncomingProtocolType
|
||||
import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsScreen
|
||||
import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsViewModel
|
||||
import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsScreen
|
||||
import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsViewModel
|
||||
import app.k9mail.feature.account.server.validation.ui.IncomingServerValidationViewModel
|
||||
import app.k9mail.feature.account.server.validation.ui.OutgoingServerValidationViewModel
|
||||
import app.k9mail.feature.account.server.validation.ui.ServerValidationScreen
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryScreen
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryViewModel
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountScreen
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountViewModel
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsScreen
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsViewModel
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsScreen
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsViewModel
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersScreen
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
private const val NESTED_NAVIGATION_AUTO_CONFIG = "autoconfig"
|
||||
private const val NESTED_NAVIGATION_INCOMING_SERVER_CONFIG = "incoming-server/config"
|
||||
private const val NESTED_NAVIGATION_INCOMING_SERVER_VALIDATION = "incoming-server/validation"
|
||||
private const val NESTED_NAVIGATION_OUTGOING_SERVER_CONFIG = "outgoing-server/config"
|
||||
private const val NESTED_NAVIGATION_OUTGOING_SERVER_VALIDATION = "outgoing-server/validation"
|
||||
private const val NESTED_NAVIGATION_SPECIAL_FOLDERS = "special-folders"
|
||||
private const val NESTED_NAVIGATION_DISPLAY_OPTIONS = "display-options"
|
||||
private const val NESTED_NAVIGATION_SYNC_OPTIONS = "sync-options"
|
||||
private const val NESTED_NAVIGATION_CREATE_ACCOUNT = "create-account"
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun AccountSetupNavHost(
|
||||
onBack: () -> Unit,
|
||||
onFinish: (AccountSetupRoute) -> Unit,
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
var isAutomaticConfig by rememberSaveable { mutableStateOf(false) }
|
||||
var hasSpecialFolders by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = NESTED_NAVIGATION_AUTO_CONFIG,
|
||||
) {
|
||||
composable(route = NESTED_NAVIGATION_AUTO_CONFIG) {
|
||||
AccountAutoDiscoveryScreen(
|
||||
onNext = { result ->
|
||||
isAutomaticConfig = result.isAutomaticConfig
|
||||
if (isAutomaticConfig) {
|
||||
hasSpecialFolders = checkSpecialFoldersSupport(result.incomingProtocolType)
|
||||
navController.navigate(NESTED_NAVIGATION_INCOMING_SERVER_VALIDATION)
|
||||
} else {
|
||||
navController.navigate(NESTED_NAVIGATION_INCOMING_SERVER_CONFIG)
|
||||
}
|
||||
},
|
||||
onBack = onBack,
|
||||
viewModel = koinViewModel<AccountAutoDiscoveryViewModel>(),
|
||||
brandNameProvider = koinInject(),
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = NESTED_NAVIGATION_INCOMING_SERVER_CONFIG) {
|
||||
IncomingServerSettingsScreen(
|
||||
onNext = { state ->
|
||||
hasSpecialFolders = checkSpecialFoldersSupport(state.protocolType)
|
||||
navController.navigate(NESTED_NAVIGATION_INCOMING_SERVER_VALIDATION)
|
||||
},
|
||||
onBack = { navController.popBackStack() },
|
||||
viewModel = koinViewModel<IncomingServerSettingsViewModel>(),
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = NESTED_NAVIGATION_INCOMING_SERVER_VALIDATION) {
|
||||
ServerValidationScreen(
|
||||
onNext = {
|
||||
if (isAutomaticConfig) {
|
||||
navController.navigate(NESTED_NAVIGATION_OUTGOING_SERVER_VALIDATION) {
|
||||
popUpTo(NESTED_NAVIGATION_AUTO_CONFIG)
|
||||
}
|
||||
} else {
|
||||
navController.navigate(NESTED_NAVIGATION_OUTGOING_SERVER_CONFIG) {
|
||||
popUpTo(NESTED_NAVIGATION_INCOMING_SERVER_CONFIG)
|
||||
}
|
||||
}
|
||||
},
|
||||
onBack = { navController.popBackStack() },
|
||||
viewModel = koinViewModel<IncomingServerValidationViewModel>(),
|
||||
brandNameProvider = koinInject(),
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = NESTED_NAVIGATION_OUTGOING_SERVER_CONFIG) {
|
||||
OutgoingServerSettingsScreen(
|
||||
onNext = { navController.navigate(NESTED_NAVIGATION_OUTGOING_SERVER_VALIDATION) },
|
||||
onBack = { navController.popBackStack() },
|
||||
viewModel = koinViewModel<OutgoingServerSettingsViewModel>(),
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = NESTED_NAVIGATION_OUTGOING_SERVER_VALIDATION) {
|
||||
ServerValidationScreen(
|
||||
onNext = {
|
||||
navController.navigate(
|
||||
if (hasSpecialFolders) {
|
||||
NESTED_NAVIGATION_SPECIAL_FOLDERS
|
||||
} else {
|
||||
NESTED_NAVIGATION_DISPLAY_OPTIONS
|
||||
},
|
||||
) {
|
||||
if (isAutomaticConfig) {
|
||||
popUpTo(NESTED_NAVIGATION_AUTO_CONFIG)
|
||||
} else {
|
||||
popUpTo(NESTED_NAVIGATION_OUTGOING_SERVER_CONFIG)
|
||||
}
|
||||
}
|
||||
},
|
||||
onBack = { navController.popBackStack() },
|
||||
viewModel = koinViewModel<OutgoingServerValidationViewModel>(),
|
||||
brandNameProvider = koinInject(),
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = NESTED_NAVIGATION_SPECIAL_FOLDERS) {
|
||||
SpecialFoldersScreen(
|
||||
onNext = { isManualSetup ->
|
||||
navController.navigate(NESTED_NAVIGATION_DISPLAY_OPTIONS) {
|
||||
if (isManualSetup) {
|
||||
popUpTo(NESTED_NAVIGATION_SPECIAL_FOLDERS)
|
||||
} else {
|
||||
if (isAutomaticConfig) {
|
||||
popUpTo(NESTED_NAVIGATION_AUTO_CONFIG)
|
||||
} else {
|
||||
popUpTo(NESTED_NAVIGATION_OUTGOING_SERVER_CONFIG)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onBack = { navController.popBackStack() },
|
||||
viewModel = koinViewModel<SpecialFoldersViewModel>(),
|
||||
brandNameProvider = koinInject(),
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = NESTED_NAVIGATION_DISPLAY_OPTIONS) {
|
||||
DisplayOptionsScreen(
|
||||
onNext = { navController.navigate(NESTED_NAVIGATION_SYNC_OPTIONS) },
|
||||
onBack = { navController.popBackStack() },
|
||||
viewModel = koinViewModel<DisplayOptionsViewModel>(),
|
||||
brandNameProvider = koinInject(),
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = NESTED_NAVIGATION_SYNC_OPTIONS) {
|
||||
SyncOptionsScreen(
|
||||
onNext = { navController.navigate(NESTED_NAVIGATION_CREATE_ACCOUNT) },
|
||||
onBack = { navController.popBackStack() },
|
||||
viewModel = koinViewModel<SyncOptionsViewModel>(),
|
||||
brandNameProvider = koinInject(),
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = NESTED_NAVIGATION_CREATE_ACCOUNT) {
|
||||
CreateAccountScreen(
|
||||
onNext = { accountUuid -> onFinish(AccountSetupRoute.AccountSetup(accountUuid.value)) },
|
||||
onBack = { navController.popBackStack() },
|
||||
viewModel = koinViewModel<CreateAccountViewModel>(),
|
||||
brandNameProvider = koinInject(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun checkSpecialFoldersSupport(protocolType: IncomingProtocolType?): Boolean {
|
||||
return protocolType == IncomingProtocolType.IMAP
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package app.k9mail.feature.account.setup.navigation
|
||||
|
||||
import app.k9mail.core.ui.compose.navigation.Navigation
|
||||
|
||||
interface AccountSetupNavigation : Navigation<AccountSetupRoute>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package app.k9mail.feature.account.setup.navigation
|
||||
|
||||
import app.k9mail.core.ui.compose.navigation.Route
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
sealed interface AccountSetupRoute : Route {
|
||||
|
||||
@Serializable
|
||||
data class AccountSetup(
|
||||
val accountId: String? = null,
|
||||
) : AccountSetupRoute {
|
||||
override val basePath: String = BASE_PATH
|
||||
|
||||
override fun route(): String = basePath
|
||||
|
||||
companion object {
|
||||
const val BASE_PATH = ACCOUNT_SETUP_BASE_PATH
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACCOUNT_SETUP_BASE_PATH = "app://account/setup"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package app.k9mail.feature.account.setup.navigation
|
||||
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import app.k9mail.core.ui.compose.navigation.deepLinkComposable
|
||||
import app.k9mail.feature.account.setup.navigation.AccountSetupRoute.AccountSetup
|
||||
|
||||
class DefaultAccountSetupNavigation : AccountSetupNavigation {
|
||||
|
||||
override fun registerRoutes(
|
||||
navGraphBuilder: NavGraphBuilder,
|
||||
onBack: () -> Unit,
|
||||
onFinish: (AccountSetupRoute) -> Unit,
|
||||
) {
|
||||
with(navGraphBuilder) {
|
||||
deepLinkComposable<AccountSetup>(
|
||||
basePath = AccountSetup.BASE_PATH,
|
||||
) {
|
||||
AccountSetupNavHost(
|
||||
onBack = onBack,
|
||||
onFinish = onFinish,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.ContentLoadingErrorView
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.ErrorView
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.LoadingView
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.EmailAddressInput
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.PasswordInput
|
||||
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import app.k9mail.feature.account.common.ui.AppTitleTopHeader
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBar
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBarState
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthView
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.State
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.view.AutoDiscoveryResultApprovalView
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.view.AutoDiscoveryResultView
|
||||
import net.thunderbird.core.ui.compose.common.modifier.testTagAsResourceId
|
||||
|
||||
@Composable
|
||||
internal fun AccountAutoDiscoveryContent(
|
||||
state: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
oAuthViewModel: AccountOAuthContract.ViewModel,
|
||||
brandName: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
ResponsiveWidthContainer(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.testTagAsResourceId("AccountAutoDiscoveryContent")
|
||||
.then(modifier),
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(scrollState)
|
||||
.padding(paddingValues)
|
||||
.imePadding(),
|
||||
) {
|
||||
AppTitleTopHeader(
|
||||
title = brandName,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
AutoDiscoveryContent(
|
||||
state = state,
|
||||
onEvent = onEvent,
|
||||
oAuthViewModel = oAuthViewModel,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
WizardNavigationBar(
|
||||
onNextClick = { onEvent(Event.OnNextClicked) },
|
||||
onBackClick = { onEvent(Event.OnBackClicked) },
|
||||
state = WizardNavigationBarState(showNext = state.isNextButtonVisible),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun AutoDiscoveryContent(
|
||||
state: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
oAuthViewModel: AccountOAuthContract.ViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
|
||||
ContentLoadingErrorView(
|
||||
state = state,
|
||||
loading = {
|
||||
LoadingView(
|
||||
message = stringResource(id = R.string.account_setup_auto_discovery_loading_message),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
},
|
||||
error = { error ->
|
||||
ErrorView(
|
||||
title = stringResource(id = R.string.account_setup_auto_discovery_loading_error),
|
||||
message = error.toAutoDiscoveryErrorString(resources),
|
||||
onRetry = { onEvent(Event.OnRetryClicked) },
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
},
|
||||
content = { contentState ->
|
||||
@Suppress("ViewModelForwarding")
|
||||
ContentView(
|
||||
state = contentState,
|
||||
onEvent = onEvent,
|
||||
oAuthViewModel = oAuthViewModel,
|
||||
resources = resources,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(modifier),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ContentView(
|
||||
state: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
oAuthViewModel: AccountOAuthContract.ViewModel,
|
||||
resources: Resources,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(MainTheme.spacings.quadruple)
|
||||
.then(modifier),
|
||||
) {
|
||||
if (state.configStep != AccountAutoDiscoveryContract.ConfigStep.EMAIL_ADDRESS) {
|
||||
AutoDiscoveryResultView(
|
||||
settings = state.autoDiscoverySettings,
|
||||
onEditConfigurationClick = { onEvent(Event.OnEditConfigurationClicked) },
|
||||
)
|
||||
if (state.autoDiscoverySettings != null && state.autoDiscoverySettings.isTrusted.not()) {
|
||||
AutoDiscoveryResultApprovalView(
|
||||
approvalState = state.configurationApproved,
|
||||
onApprovalChange = { onEvent(Event.ResultApprovalChanged(it)) },
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(MainTheme.spacings.double))
|
||||
}
|
||||
|
||||
EmailAddressInput(
|
||||
emailAddress = state.emailAddress.value,
|
||||
errorMessage = state.emailAddress.error?.toAutoDiscoveryValidationErrorString(resources),
|
||||
onEmailAddressChange = { onEvent(Event.EmailAddressChanged(it)) },
|
||||
contentPadding = PaddingValues(),
|
||||
modifier = Modifier.testTagAsResourceId("account_setup_email_address_input"),
|
||||
)
|
||||
|
||||
if (state.configStep == AccountAutoDiscoveryContract.ConfigStep.PASSWORD) {
|
||||
Spacer(modifier = Modifier.height(MainTheme.spacings.double))
|
||||
PasswordInput(
|
||||
password = state.password.value,
|
||||
errorMessage = state.password.error?.toAutoDiscoveryValidationErrorString(resources),
|
||||
onPasswordChange = { onEvent(Event.PasswordChanged(it)) },
|
||||
contentPadding = PaddingValues(),
|
||||
modifier = Modifier.testTagAsResourceId("account_setup_password_input"),
|
||||
)
|
||||
} else if (state.configStep == AccountAutoDiscoveryContract.ConfigStep.OAUTH) {
|
||||
val isAutoDiscoverySettingsTrusted = state.autoDiscoverySettings?.isTrusted ?: false
|
||||
val isConfigurationApproved = state.configurationApproved.value ?: false
|
||||
Spacer(modifier = Modifier.height(MainTheme.spacings.double))
|
||||
AccountOAuthView(
|
||||
onOAuthResult = { result -> onEvent(Event.OnOAuthResult(result)) },
|
||||
viewModel = oAuthViewModel,
|
||||
isEnabled = isAutoDiscoverySettingsTrusted || isConfigurationApproved,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery
|
||||
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.LoadingErrorState
|
||||
import app.k9mail.feature.account.common.domain.entity.AuthorizationState
|
||||
import app.k9mail.feature.account.common.domain.entity.IncomingProtocolType
|
||||
import app.k9mail.feature.account.common.domain.input.BooleanInputField
|
||||
import app.k9mail.feature.account.common.domain.input.StringInputField
|
||||
import app.k9mail.feature.account.oauth.domain.entity.OAuthResult
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
interface AccountAutoDiscoveryContract {
|
||||
|
||||
enum class ConfigStep {
|
||||
EMAIL_ADDRESS,
|
||||
OAUTH,
|
||||
PASSWORD,
|
||||
MANUAL_SETUP,
|
||||
}
|
||||
|
||||
interface ViewModel : UnidirectionalViewModel<State, Event, Effect> {
|
||||
val oAuthViewModel: AccountOAuthContract.ViewModel
|
||||
|
||||
fun initState(state: State)
|
||||
}
|
||||
|
||||
data class State(
|
||||
val configStep: ConfigStep = ConfigStep.EMAIL_ADDRESS,
|
||||
val emailAddress: StringInputField = StringInputField(),
|
||||
val password: StringInputField = StringInputField(),
|
||||
val autoDiscoverySettings: AutoDiscoveryResult.Settings? = null,
|
||||
val configurationApproved: BooleanInputField = BooleanInputField(),
|
||||
val authorizationState: AuthorizationState? = null,
|
||||
|
||||
val isSuccess: Boolean = false,
|
||||
override val error: Error? = null,
|
||||
override val isLoading: Boolean = false,
|
||||
|
||||
val isNextButtonVisible: Boolean = true,
|
||||
) : LoadingErrorState<Error>
|
||||
|
||||
sealed interface Event {
|
||||
data class EmailAddressChanged(val emailAddress: String) : Event
|
||||
data class PasswordChanged(val password: String) : Event
|
||||
data class ResultApprovalChanged(val confirmed: Boolean) : Event
|
||||
data class OnOAuthResult(val result: OAuthResult) : Event
|
||||
|
||||
data object OnNextClicked : Event
|
||||
data object OnBackClicked : Event
|
||||
data object OnRetryClicked : Event
|
||||
data object OnEditConfigurationClicked : Event
|
||||
}
|
||||
|
||||
sealed class Effect {
|
||||
data class NavigateNext(
|
||||
val result: AutoDiscoveryUiResult,
|
||||
) : Effect()
|
||||
|
||||
data object NavigateBack : Effect()
|
||||
}
|
||||
|
||||
interface Validator {
|
||||
fun validateEmailAddress(emailAddress: String): ValidationResult
|
||||
fun validatePassword(password: String): ValidationResult
|
||||
fun validateConfigurationApproval(isApproved: Boolean?, isAutoDiscoveryTrusted: Boolean?): ValidationResult
|
||||
}
|
||||
|
||||
sealed interface Error {
|
||||
data object NetworkError : Error
|
||||
data object UnknownError : Error
|
||||
}
|
||||
|
||||
data class AutoDiscoveryUiResult(
|
||||
val isAutomaticConfig: Boolean,
|
||||
val incomingProtocolType: IncomingProtocolType?,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.k9mail.core.ui.compose.common.mvi.observe
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.AutoDiscoveryUiResult
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.ViewModel
|
||||
import net.thunderbird.core.common.provider.BrandNameProvider
|
||||
|
||||
@Composable
|
||||
internal fun AccountAutoDiscoveryScreen(
|
||||
onNext: (AutoDiscoveryUiResult) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ViewModel,
|
||||
brandNameProvider: BrandNameProvider,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (state, dispatch) = viewModel.observe { effect ->
|
||||
when (effect) {
|
||||
Effect.NavigateBack -> onBack()
|
||||
is Effect.NavigateNext -> onNext(effect.result)
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
dispatch(Event.OnBackClicked)
|
||||
}
|
||||
|
||||
AccountAutoDiscoveryContent(
|
||||
state = state.value,
|
||||
onEvent = { dispatch(it) },
|
||||
oAuthViewModel = viewModel.oAuthViewModel,
|
||||
brandName = brandNameProvider.brandName,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery
|
||||
|
||||
import app.k9mail.autodiscovery.api.ImapServerSettings
|
||||
import app.k9mail.autodiscovery.api.SmtpServerSettings
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountState
|
||||
import app.k9mail.feature.account.common.domain.input.NumberInputField
|
||||
import app.k9mail.feature.account.common.domain.input.StringInputField
|
||||
import app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsContract
|
||||
import app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsContract
|
||||
import app.k9mail.feature.account.setup.domain.entity.toAuthenticationType
|
||||
import app.k9mail.feature.account.setup.domain.entity.toConnectionSecurity
|
||||
import app.k9mail.feature.account.setup.domain.entity.toIncomingProtocolType
|
||||
import app.k9mail.feature.account.setup.domain.toServerSettings
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract
|
||||
|
||||
internal fun AccountAutoDiscoveryContract.State.toAccountState(): AccountState {
|
||||
return AccountState(
|
||||
emailAddress = emailAddress.value,
|
||||
incomingServerSettings = autoDiscoverySettings?.incomingServerSettings?.toServerSettings(password.value),
|
||||
outgoingServerSettings = autoDiscoverySettings?.outgoingServerSettings?.toServerSettings(password.value),
|
||||
authorizationState = authorizationState,
|
||||
displayOptions = null,
|
||||
syncOptions = null,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun AccountAutoDiscoveryContract.State.toIncomingConfigState(): IncomingServerSettingsContract.State {
|
||||
val incomingSettings = autoDiscoverySettings?.incomingServerSettings as? ImapServerSettings?
|
||||
return if (incomingSettings == null) {
|
||||
IncomingServerSettingsContract.State(
|
||||
username = StringInputField(value = emailAddress.value),
|
||||
password = StringInputField(value = password.value),
|
||||
)
|
||||
} else {
|
||||
IncomingServerSettingsContract.State(
|
||||
protocolType = incomingSettings.toIncomingProtocolType(),
|
||||
server = StringInputField(value = incomingSettings.hostname.value),
|
||||
security = incomingSettings.connectionSecurity.toConnectionSecurity(),
|
||||
port = NumberInputField(value = incomingSettings.port.value.toLong()),
|
||||
authenticationType = incomingSettings.authenticationTypes.first().toAuthenticationType(),
|
||||
username = StringInputField(value = incomingSettings.username),
|
||||
password = StringInputField(value = password.value),
|
||||
imapAutodetectNamespaceEnabled = true,
|
||||
imapPrefix = StringInputField(value = ""),
|
||||
imapUseCompression = true,
|
||||
imapSendClientInfo = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun AccountAutoDiscoveryContract.State.toOutgoingConfigState(): OutgoingServerSettingsContract.State {
|
||||
val outgoingSettings = autoDiscoverySettings?.outgoingServerSettings as? SmtpServerSettings?
|
||||
return if (outgoingSettings == null) {
|
||||
OutgoingServerSettingsContract.State(
|
||||
username = StringInputField(value = emailAddress.value),
|
||||
password = StringInputField(value = password.value),
|
||||
)
|
||||
} else {
|
||||
OutgoingServerSettingsContract.State(
|
||||
server = StringInputField(value = outgoingSettings.hostname.value),
|
||||
security = outgoingSettings.connectionSecurity.toConnectionSecurity(),
|
||||
port = NumberInputField(value = outgoingSettings.port.value.toLong()),
|
||||
authenticationType = outgoingSettings.authenticationTypes.first().toAuthenticationType(),
|
||||
username = StringInputField(value = outgoingSettings.username),
|
||||
password = StringInputField(value = password.value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun AccountAutoDiscoveryContract.State.toOptionsState(): DisplayOptionsContract.State {
|
||||
return DisplayOptionsContract.State(
|
||||
accountName = StringInputField(value = emailAddress.value),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery
|
||||
|
||||
import app.k9mail.feature.account.server.settings.domain.usecase.ValidatePassword
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateConfigurationApproval
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailAddress
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
import app.k9mail.feature.account.server.settings.domain.ServerSettingsDomainContract.UseCase as ServerSettingsUseCase
|
||||
|
||||
internal class AccountAutoDiscoveryValidator(
|
||||
private val emailAddressValidator: UseCase.ValidateEmailAddress = ValidateEmailAddress(),
|
||||
private val passwordValidator: ServerSettingsUseCase.ValidatePassword = ValidatePassword(),
|
||||
private val configurationApprovalValidator: UseCase.ValidateConfigurationApproval = ValidateConfigurationApproval(),
|
||||
) : AccountAutoDiscoveryContract.Validator {
|
||||
|
||||
override fun validateEmailAddress(emailAddress: String): ValidationResult {
|
||||
return emailAddressValidator.execute(emailAddress)
|
||||
}
|
||||
|
||||
override fun validatePassword(password: String): ValidationResult {
|
||||
return passwordValidator.execute(password)
|
||||
}
|
||||
|
||||
override fun validateConfigurationApproval(
|
||||
isApproved: Boolean?,
|
||||
isAutoDiscoveryTrusted: Boolean?,
|
||||
): ValidationResult {
|
||||
return configurationApprovalValidator.execute(isApproved, isAutoDiscoveryTrusted)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.autodiscovery.api.ImapServerSettings
|
||||
import app.k9mail.autodiscovery.api.IncomingServerSettings
|
||||
import app.k9mail.autodiscovery.demo.DemoServerSettings
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.common.domain.AccountDomainContract
|
||||
import app.k9mail.feature.account.common.domain.entity.IncomingProtocolType
|
||||
import app.k9mail.feature.account.common.domain.input.StringInputField
|
||||
import app.k9mail.feature.account.oauth.domain.entity.OAuthResult
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase
|
||||
import app.k9mail.feature.account.setup.domain.entity.AutoDiscoveryAuthenticationType
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.AutoDiscoveryUiResult
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.ConfigStep
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Error
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.State
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract.Validator
|
||||
import kotlinx.coroutines.launch
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
internal class AccountAutoDiscoveryViewModel(
|
||||
initialState: State = State(),
|
||||
private val validator: Validator,
|
||||
private val getAutoDiscovery: UseCase.GetAutoDiscovery,
|
||||
private val accountStateRepository: AccountDomainContract.AccountStateRepository,
|
||||
override val oAuthViewModel: AccountOAuthContract.ViewModel,
|
||||
) : BaseViewModel<State, Event, Effect>(initialState), AccountAutoDiscoveryContract.ViewModel {
|
||||
|
||||
override fun initState(state: State) {
|
||||
updateState {
|
||||
state.copy()
|
||||
}
|
||||
}
|
||||
|
||||
override fun event(event: Event) {
|
||||
when (event) {
|
||||
is Event.EmailAddressChanged -> changeEmailAddress(event.emailAddress)
|
||||
is Event.PasswordChanged -> changePassword(event.password)
|
||||
is Event.ResultApprovalChanged -> changeConfigurationApproval(event.confirmed)
|
||||
is Event.OnOAuthResult -> onOAuthResult(event.result)
|
||||
|
||||
Event.OnNextClicked -> onNext()
|
||||
Event.OnBackClicked -> onBack()
|
||||
Event.OnRetryClicked -> onRetry()
|
||||
Event.OnEditConfigurationClicked -> {
|
||||
navigateNext(isAutomaticConfig = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeEmailAddress(emailAddress: String) {
|
||||
accountStateRepository.clear()
|
||||
updateState {
|
||||
State(
|
||||
emailAddress = StringInputField(value = emailAddress),
|
||||
isNextButtonVisible = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun changePassword(password: String) {
|
||||
updateState {
|
||||
it.copy(
|
||||
password = it.password.updateValue(password),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeConfigurationApproval(approved: Boolean) {
|
||||
updateState {
|
||||
it.copy(
|
||||
configurationApproved = it.configurationApproved.updateValue(approved),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNext() {
|
||||
when (state.value.configStep) {
|
||||
ConfigStep.EMAIL_ADDRESS ->
|
||||
if (state.value.error != null) {
|
||||
updateState {
|
||||
it.copy(
|
||||
error = null,
|
||||
configStep = ConfigStep.PASSWORD,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
submitEmail()
|
||||
}
|
||||
|
||||
ConfigStep.PASSWORD -> submitPassword()
|
||||
ConfigStep.OAUTH -> Unit
|
||||
ConfigStep.MANUAL_SETUP -> navigateNext(isAutomaticConfig = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRetry() {
|
||||
updateState {
|
||||
it.copy(error = null)
|
||||
}
|
||||
loadAutoDiscovery()
|
||||
}
|
||||
|
||||
private fun submitEmail() {
|
||||
with(state.value) {
|
||||
val emailValidationResult = validator.validateEmailAddress(emailAddress.value)
|
||||
val hasError = emailValidationResult is ValidationResult.Failure
|
||||
|
||||
updateState {
|
||||
it.copy(
|
||||
emailAddress = it.emailAddress.updateFromValidationResult(emailValidationResult),
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasError) {
|
||||
loadAutoDiscovery()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAutoDiscovery() {
|
||||
viewModelScope.launch {
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = true,
|
||||
)
|
||||
}
|
||||
|
||||
val result = getAutoDiscovery.execute(state.value.emailAddress.value)
|
||||
when (result) {
|
||||
AutoDiscoveryResult.NoUsableSettingsFound -> updateNoSettingsFound()
|
||||
is AutoDiscoveryResult.Settings -> updateAutoDiscoverySettings(result)
|
||||
is AutoDiscoveryResult.NetworkError -> updateError(Error.NetworkError)
|
||||
is AutoDiscoveryResult.UnexpectedException -> updateError(Error.UnknownError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNoSettingsFound() {
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
autoDiscoverySettings = null,
|
||||
configStep = ConfigStep.MANUAL_SETUP,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAutoDiscoverySettings(settings: AutoDiscoveryResult.Settings) {
|
||||
if (settings.incomingServerSettings is DemoServerSettings) {
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
autoDiscoverySettings = settings,
|
||||
configStep = ConfigStep.PASSWORD,
|
||||
isNextButtonVisible = true,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val imapServerSettings = settings.incomingServerSettings as ImapServerSettings
|
||||
val isOAuth = imapServerSettings.authenticationTypes.first() == AutoDiscoveryAuthenticationType.OAuth2
|
||||
|
||||
if (isOAuth) {
|
||||
oAuthViewModel.initState(
|
||||
AccountOAuthContract.State(
|
||||
hostname = imapServerSettings.hostname.value,
|
||||
emailAddress = state.value.emailAddress.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
autoDiscoverySettings = settings,
|
||||
configStep = if (isOAuth) ConfigStep.OAUTH else ConfigStep.PASSWORD,
|
||||
isNextButtonVisible = !isOAuth,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateError(error: Error) {
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitPassword() {
|
||||
with(state.value) {
|
||||
val emailValidationResult = validator.validateEmailAddress(emailAddress.value)
|
||||
val passwordValidationResult = validator.validatePassword(password.value)
|
||||
val configurationApprovalValidationResult = validator.validateConfigurationApproval(
|
||||
isApproved = configurationApproved.value,
|
||||
isAutoDiscoveryTrusted = autoDiscoverySettings?.isTrusted,
|
||||
)
|
||||
val hasError = listOf(
|
||||
emailValidationResult,
|
||||
passwordValidationResult,
|
||||
configurationApprovalValidationResult,
|
||||
).any { it is ValidationResult.Failure }
|
||||
|
||||
updateState {
|
||||
it.copy(
|
||||
emailAddress = it.emailAddress.updateFromValidationResult(emailValidationResult),
|
||||
password = it.password.updateFromValidationResult(passwordValidationResult),
|
||||
configurationApproved = it.configurationApproved.updateFromValidationResult(
|
||||
configurationApprovalValidationResult,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasError) {
|
||||
navigateNext(state.value.autoDiscoverySettings != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBack() {
|
||||
when (state.value.configStep) {
|
||||
ConfigStep.EMAIL_ADDRESS -> {
|
||||
if (state.value.error != null) {
|
||||
updateState {
|
||||
it.copy(error = null)
|
||||
}
|
||||
} else {
|
||||
navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
ConfigStep.OAUTH,
|
||||
ConfigStep.PASSWORD,
|
||||
ConfigStep.MANUAL_SETUP,
|
||||
-> updateState {
|
||||
it.copy(
|
||||
configStep = ConfigStep.EMAIL_ADDRESS,
|
||||
password = StringInputField(),
|
||||
isNextButtonVisible = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOAuthResult(result: OAuthResult) {
|
||||
if (result is OAuthResult.Success) {
|
||||
updateState {
|
||||
it.copy(authorizationState = result.authorizationState)
|
||||
}
|
||||
|
||||
navigateNext(isAutomaticConfig = true)
|
||||
} else {
|
||||
updateState {
|
||||
it.copy(authorizationState = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateBack() = emitEffect(Effect.NavigateBack)
|
||||
|
||||
private fun navigateNext(isAutomaticConfig: Boolean) {
|
||||
accountStateRepository.setState(state.value.toAccountState())
|
||||
|
||||
emitEffect(
|
||||
Effect.NavigateNext(
|
||||
result = mapToAutoDiscoveryResult(
|
||||
isAutomaticConfig = isAutomaticConfig,
|
||||
incomingServerSettings = state.value.autoDiscoverySettings?.incomingServerSettings,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapToAutoDiscoveryResult(
|
||||
isAutomaticConfig: Boolean,
|
||||
incomingServerSettings: IncomingServerSettings?,
|
||||
): AutoDiscoveryUiResult {
|
||||
val incomingProtocolType = if (incomingServerSettings is ImapServerSettings) {
|
||||
IncomingProtocolType.IMAP
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return AutoDiscoveryUiResult(
|
||||
isAutomaticConfig = isAutomaticConfig,
|
||||
incomingProtocolType = incomingProtocolType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery
|
||||
|
||||
import android.content.res.Resources
|
||||
import app.k9mail.feature.account.server.settings.domain.usecase.ValidatePassword
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.domain.entity.AutoDiscoveryConnectionSecurity
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateConfigurationApproval
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailAddress
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationError
|
||||
|
||||
internal fun AutoDiscoveryConnectionSecurity.toAutoDiscoveryConnectionSecurityString(resources: Resources): String {
|
||||
return when (this) {
|
||||
AutoDiscoveryConnectionSecurity.StartTLS -> resources.getString(
|
||||
R.string.account_setup_auto_discovery_connection_security_start_tls,
|
||||
)
|
||||
|
||||
AutoDiscoveryConnectionSecurity.TLS -> resources.getString(
|
||||
R.string.account_setup_auto_discovery_connection_security_ssl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun AccountAutoDiscoveryContract.Error.toAutoDiscoveryErrorString(resources: Resources): String {
|
||||
return when (this) {
|
||||
AccountAutoDiscoveryContract.Error.NetworkError -> resources.getString(R.string.account_setup_error_network)
|
||||
AccountAutoDiscoveryContract.Error.UnknownError -> resources.getString(R.string.account_setup_error_unknown)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ValidationError.toAutoDiscoveryValidationErrorString(resources: Resources): String {
|
||||
return when (this) {
|
||||
is ValidateEmailAddress.ValidateEmailAddressError -> toEmailAddressErrorString(resources)
|
||||
is ValidatePassword.ValidatePasswordError -> toPasswordErrorString(resources)
|
||||
|
||||
is ValidateConfigurationApproval.ValidateConfigurationApprovalError -> toConfigurationApprovalErrorString(
|
||||
resources,
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown error: $this")
|
||||
}
|
||||
}
|
||||
|
||||
private fun ValidateEmailAddress.ValidateEmailAddressError.toEmailAddressErrorString(resources: Resources): String {
|
||||
return when (this) {
|
||||
ValidateEmailAddress.ValidateEmailAddressError.EmptyEmailAddress -> {
|
||||
resources.getString(R.string.account_setup_auto_discovery_validation_error_email_address_required)
|
||||
}
|
||||
|
||||
ValidateEmailAddress.ValidateEmailAddressError.NotAllowed -> {
|
||||
resources.getString(R.string.account_setup_auto_discovery_validation_error_email_address_not_allowed)
|
||||
}
|
||||
|
||||
ValidateEmailAddress.ValidateEmailAddressError.InvalidOrNotSupported -> {
|
||||
resources.getString(R.string.account_setup_auto_discovery_validation_error_email_address_not_supported)
|
||||
}
|
||||
|
||||
ValidateEmailAddress.ValidateEmailAddressError.InvalidEmailAddress -> {
|
||||
resources.getString(R.string.account_setup_auto_discovery_validation_error_email_address_invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ValidatePassword.ValidatePasswordError.toPasswordErrorString(resources: Resources): String {
|
||||
return when (this) {
|
||||
ValidatePassword.ValidatePasswordError.EmptyPassword -> resources.getString(
|
||||
R.string.account_setup_auto_discovery_validation_error_password_required,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ValidateConfigurationApproval.ValidateConfigurationApprovalError.toConfigurationApprovalErrorString(
|
||||
resources: Resources,
|
||||
): String {
|
||||
return when (this) {
|
||||
ValidateConfigurationApproval.ValidateConfigurationApprovalError.ApprovalRequired -> resources.getString(
|
||||
R.string.account_setup_auto_discovery_result_approval_error_approval_required,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery.view
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.CheckboxInput
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import app.k9mail.feature.account.common.domain.input.BooleanInputField
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.toAutoDiscoveryValidationErrorString
|
||||
|
||||
@Composable
|
||||
internal fun AutoDiscoveryResultApprovalView(
|
||||
approvalState: BooleanInputField,
|
||||
onApprovalChange: (Boolean) -> Unit,
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
|
||||
Spacer(modifier = Modifier.height(MainTheme.spacings.default))
|
||||
|
||||
CheckboxInput(
|
||||
text = stringResource(
|
||||
id = R.string.account_setup_auto_discovery_result_approval_checkbox_label,
|
||||
),
|
||||
checked = approvalState.value ?: false,
|
||||
onCheckedChange = onApprovalChange,
|
||||
errorMessage = approvalState.error?.toAutoDiscoveryValidationErrorString(resources),
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery.view
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.autodiscovery.api.ImapServerSettings
|
||||
import app.k9mail.autodiscovery.api.SmtpServerSettings
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonText
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import app.k9mail.feature.account.setup.R
|
||||
|
||||
@Composable
|
||||
internal fun AutoDiscoveryResultBodyView(
|
||||
settings: AutoDiscoveryResult.Settings,
|
||||
onEditConfigurationClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = MainTheme.spacings.default)
|
||||
.then(modifier),
|
||||
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
|
||||
) {
|
||||
if (settings.isTrusted.not()) {
|
||||
Spacer(modifier = Modifier.height(MainTheme.sizes.smaller))
|
||||
TextBodyMedium(
|
||||
text = stringResource(
|
||||
id = R.string.account_setup_auto_discovery_result_disclaimer_untrusted_configuration,
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
val incomingServerSettings = settings.incomingServerSettings
|
||||
if (incomingServerSettings is ImapServerSettings) {
|
||||
Spacer(modifier = Modifier.height(MainTheme.sizes.smaller))
|
||||
AutoDiscoveryServerSettingsView(
|
||||
protocolName = "IMAP",
|
||||
serverHostname = incomingServerSettings.hostname,
|
||||
serverPort = incomingServerSettings.port.value,
|
||||
connectionSecurity = incomingServerSettings.connectionSecurity,
|
||||
username = incomingServerSettings.username,
|
||||
isIncoming = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
val outgoingServerSettings = settings.outgoingServerSettings
|
||||
if (outgoingServerSettings is SmtpServerSettings) {
|
||||
Spacer(modifier = Modifier.height(MainTheme.sizes.smaller))
|
||||
AutoDiscoveryServerSettingsView(
|
||||
protocolName = "SMTP",
|
||||
serverHostname = outgoingServerSettings.hostname,
|
||||
serverPort = outgoingServerSettings.port.value,
|
||||
connectionSecurity = outgoingServerSettings.connectionSecurity,
|
||||
username = outgoingServerSettings.username,
|
||||
isIncoming = false,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
EditConfigurationButton(
|
||||
onEditConfigurationClick = onEditConfigurationClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun EditConfigurationButton(
|
||||
modifier: Modifier = Modifier,
|
||||
onEditConfigurationClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(modifier),
|
||||
) {
|
||||
ButtonText(
|
||||
text = stringResource(id = R.string.account_setup_auto_discovery_result_edit_configuration_button_label),
|
||||
onClick = onEditConfigurationClick,
|
||||
color = MainTheme.colors.warning,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery.view
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
|
||||
import app.k9mail.feature.account.setup.R
|
||||
|
||||
@Suppress("detekt.UnnecessaryAnnotationUseSiteTarget") // https://github.com/detekt/detekt/issues/8212
|
||||
enum class AutoDiscoveryResultHeaderState(
|
||||
val icon: ImageVector,
|
||||
@param:StringRes val titleResourceId: Int,
|
||||
@param:StringRes val subtitleResourceId: Int,
|
||||
val isExpandable: Boolean,
|
||||
) {
|
||||
NoSettings(
|
||||
icon = Icons.Outlined.Info,
|
||||
titleResourceId = R.string.account_setup_auto_discovery_result_header_title_configuration_not_found,
|
||||
subtitleResourceId = R.string.account_setup_auto_discovery_result_header_subtitle_configuration_not_found,
|
||||
isExpandable = false,
|
||||
),
|
||||
|
||||
Trusted(
|
||||
icon = Icons.Outlined.Check,
|
||||
titleResourceId = R.string.account_setup_auto_discovery_status_header_title_configuration_found,
|
||||
subtitleResourceId = R.string.account_setup_auto_discovery_result_header_subtitle_configuration_trusted,
|
||||
isExpandable = true,
|
||||
),
|
||||
|
||||
Untrusted(
|
||||
icon = Icons.Outlined.Info,
|
||||
titleResourceId = R.string.account_setup_auto_discovery_status_header_title_configuration_found,
|
||||
subtitleResourceId = R.string.account_setup_auto_discovery_result_header_subtitle_configuration_untrusted,
|
||||
isExpandable = true,
|
||||
),
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
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.res.stringResource
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icon
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleLarge
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun AutoDiscoveryResultHeaderView(
|
||||
state: AutoDiscoveryResultHeaderState,
|
||||
isExpanded: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(modifier),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = state.icon,
|
||||
tint = selectColor(state),
|
||||
modifier = Modifier
|
||||
.padding(MainTheme.spacings.default)
|
||||
.requiredSize(MainTheme.sizes.medium),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
start = MainTheme.spacings.default,
|
||||
top = MainTheme.spacings.half,
|
||||
bottom = MainTheme.spacings.half,
|
||||
),
|
||||
) {
|
||||
TextTitleLarge(
|
||||
text = stringResource(state.titleResourceId),
|
||||
)
|
||||
TextBodyMedium(
|
||||
text = stringResource(state.subtitleResourceId),
|
||||
)
|
||||
}
|
||||
if (state.isExpandable) {
|
||||
Icon(
|
||||
imageVector = if (isExpanded) Icons.Outlined.ExpandLess else Icons.Outlined.ExpandMore,
|
||||
modifier = Modifier.padding(MainTheme.spacings.default),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun selectColor(state: AutoDiscoveryResultHeaderState): Color {
|
||||
return when (state) {
|
||||
AutoDiscoveryResultHeaderState.NoSettings -> MainTheme.colors.primary
|
||||
AutoDiscoveryResultHeaderState.Trusted -> MainTheme.colors.success
|
||||
AutoDiscoveryResultHeaderState.Untrusted -> MainTheme.colors.warning
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery.view
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.Surface
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
|
||||
@Composable
|
||||
internal fun AutoDiscoveryResultView(
|
||||
settings: AutoDiscoveryResult.Settings?,
|
||||
onEditConfigurationClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val expanded = rememberSaveable {
|
||||
mutableStateOf(settings?.isTrusted?.not() ?: false)
|
||||
}
|
||||
|
||||
val discoveryResultHeaderState = if (settings == null) {
|
||||
AutoDiscoveryResultHeaderState.NoSettings
|
||||
} else if (settings.isTrusted) {
|
||||
AutoDiscoveryResultHeaderState.Trusted
|
||||
} else {
|
||||
AutoDiscoveryResultHeaderState.Untrusted
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Surface(
|
||||
shape = MainTheme.shapes.small,
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = Color.Gray.copy(alpha = 0.5f),
|
||||
shape = MainTheme.shapes.small,
|
||||
).let {
|
||||
if (discoveryResultHeaderState.isExpandable) {
|
||||
it.clickable(enabled = true) { expanded.value = !expanded.value }
|
||||
} else if (discoveryResultHeaderState == AutoDiscoveryResultHeaderState.NoSettings) {
|
||||
it.clickable(enabled = true) { onEditConfigurationClick() }
|
||||
} else {
|
||||
it.clickable(enabled = false) {}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(MainTheme.spacings.default),
|
||||
) {
|
||||
AutoDiscoveryResultHeaderView(
|
||||
state = discoveryResultHeaderState,
|
||||
isExpanded = expanded.value,
|
||||
)
|
||||
|
||||
if (settings != null) {
|
||||
AnimatedVisibility(visible = expanded.value) {
|
||||
AutoDiscoveryResultBodyView(
|
||||
settings = settings,
|
||||
onEditConfigurationClick = onEditConfigurationClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package app.k9mail.feature.account.setup.ui.autodiscovery.view
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import app.k9mail.autodiscovery.api.ConnectionSecurity
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icon
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyLarge
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.toAutoDiscoveryConnectionSecurityString
|
||||
import net.thunderbird.core.common.net.Hostname
|
||||
import net.thunderbird.core.common.net.isIpAddress
|
||||
|
||||
@Composable
|
||||
internal fun AutoDiscoveryServerSettingsView(
|
||||
protocolName: String,
|
||||
serverHostname: Hostname,
|
||||
serverPort: Int,
|
||||
connectionSecurity: ConnectionSecurity,
|
||||
modifier: Modifier = Modifier,
|
||||
username: String = "",
|
||||
isIncoming: Boolean = true,
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
|
||||
modifier = modifier,
|
||||
) {
|
||||
TextBodyLarge(
|
||||
text = buildAnnotatedString {
|
||||
append(if (isIncoming) "Incoming" else "Outgoing")
|
||||
append(" ")
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(protocolName.uppercase())
|
||||
}
|
||||
append(" ")
|
||||
append("configuration")
|
||||
},
|
||||
)
|
||||
|
||||
ServerSettingRow(
|
||||
icon = if (isIncoming) Icons.Outlined.Inbox else Icons.Outlined.Outbox,
|
||||
text = buildAnnotatedString {
|
||||
append("Server")
|
||||
append(": ")
|
||||
if (serverHostname.isIpAddress()) {
|
||||
append(serverHostname.value)
|
||||
} else {
|
||||
append(serverHostname.value.substringBefore(".") + ".")
|
||||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(serverHostname.value.substringAfter("."))
|
||||
}
|
||||
}
|
||||
append(":$serverPort")
|
||||
},
|
||||
)
|
||||
|
||||
ServerSettingRow(
|
||||
icon = Icons.Outlined.Security,
|
||||
text = buildAnnotatedString {
|
||||
append("Security: ")
|
||||
append(connectionSecurity.toAutoDiscoveryConnectionSecurityString(resources))
|
||||
},
|
||||
)
|
||||
|
||||
if (username.isNotEmpty()) {
|
||||
ServerSettingRow(
|
||||
icon = Icons.Outlined.AccountCircle,
|
||||
text = buildAnnotatedString {
|
||||
append("Username: ")
|
||||
append(username)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServerSettingRow(
|
||||
icon: ImageVector,
|
||||
text: AnnotatedString,
|
||||
modifier: Modifier = Modifier,
|
||||
showIcon: Boolean = false,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(modifier),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (showIcon) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
modifier = Modifier.padding(end = MainTheme.spacings.default),
|
||||
)
|
||||
}
|
||||
TextBodyMedium(
|
||||
text = text,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.molecule.ContentLoadingErrorView
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.ErrorView
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.LoadingView
|
||||
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import net.thunderbird.core.ui.compose.common.modifier.testTagAsResourceId
|
||||
|
||||
@Composable
|
||||
internal fun CreateAccountContent(
|
||||
state: CreateAccountContract.State,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ResponsiveWidthContainer(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.testTagAsResourceId("CreateAccountContent")
|
||||
.then(modifier),
|
||||
) { contentPadding ->
|
||||
ContentLoadingErrorView(
|
||||
state = state,
|
||||
loading = {
|
||||
LoadingView(
|
||||
message = stringResource(R.string.account_setup_create_account_creating),
|
||||
)
|
||||
},
|
||||
error = {
|
||||
ErrorView(
|
||||
title = stringResource(R.string.account_setup_create_account_error),
|
||||
)
|
||||
},
|
||||
content = {
|
||||
LoadingView(
|
||||
message = stringResource(R.string.account_setup_create_account_created),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize().padding(contentPadding),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.LoadingErrorState
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult.Error
|
||||
import app.k9mail.feature.account.setup.domain.entity.AccountUuid
|
||||
|
||||
interface CreateAccountContract {
|
||||
|
||||
interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
|
||||
|
||||
data class State(
|
||||
override val isLoading: Boolean = true,
|
||||
override val error: Error? = null,
|
||||
) : LoadingErrorState<Error>
|
||||
|
||||
sealed interface Event {
|
||||
data object CreateAccount : Event
|
||||
data object OnBackClicked : Event
|
||||
}
|
||||
|
||||
sealed interface Effect {
|
||||
data class NavigateNext(val accountUuid: AccountUuid) : Effect
|
||||
data object NavigateBack : Effect
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.k9mail.core.ui.compose.common.mvi.observe
|
||||
import app.k9mail.core.ui.compose.designsystem.template.Scaffold
|
||||
import app.k9mail.feature.account.common.ui.AppTitleTopHeader
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBar
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBarState
|
||||
import app.k9mail.feature.account.setup.domain.entity.AccountUuid
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.ViewModel
|
||||
import net.thunderbird.core.common.provider.BrandNameProvider
|
||||
|
||||
@Composable
|
||||
internal fun CreateAccountScreen(
|
||||
onNext: (AccountUuid) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ViewModel,
|
||||
brandNameProvider: BrandNameProvider,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (state, dispatch) = viewModel.observe { effect ->
|
||||
when (effect) {
|
||||
Effect.NavigateBack -> onBack()
|
||||
is Effect.NavigateNext -> onNext(effect.accountUuid)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
dispatch(Event.CreateAccount)
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
dispatch(Event.OnBackClicked)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTitleTopHeader(
|
||||
title = brandNameProvider.brandName,
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
WizardNavigationBar(
|
||||
onNextClick = {},
|
||||
onBackClick = {
|
||||
dispatch(Event.OnBackClicked)
|
||||
},
|
||||
state = WizardNavigationBarState(
|
||||
showNext = false,
|
||||
isBackEnabled = state.value.error != null,
|
||||
),
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) { innerPadding ->
|
||||
CreateAccountContent(
|
||||
state = state.value,
|
||||
contentPadding = innerPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package app.k9mail.feature.account.setup.ui.createaccount
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.common.domain.AccountDomainContract.AccountStateRepository
|
||||
import app.k9mail.feature.account.common.ui.WizardConstants
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase.CreateAccount
|
||||
import app.k9mail.feature.account.setup.domain.entity.AccountUuid
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.createaccount.CreateAccountContract.State
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CreateAccountViewModel(
|
||||
private val createAccount: CreateAccount,
|
||||
private val accountStateRepository: AccountStateRepository,
|
||||
initialState: State = State(),
|
||||
) : BaseViewModel<State, Event, Effect>(initialState),
|
||||
CreateAccountContract.ViewModel {
|
||||
|
||||
override fun event(event: Event) {
|
||||
when (event) {
|
||||
Event.CreateAccount -> handleOneTimeEvent(event, ::createAccount)
|
||||
Event.OnBackClicked -> maybeNavigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAccount() {
|
||||
val accountState = accountStateRepository.getState()
|
||||
|
||||
viewModelScope.launch {
|
||||
when (val result = createAccount.execute(accountState)) {
|
||||
is AccountCreatorResult.Success -> showSuccess(AccountUuid(result.accountUuid))
|
||||
is AccountCreatorResult.Error -> showError(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSuccess(accountUuid: AccountUuid) {
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
delay(WizardConstants.CONTINUE_NEXT_DELAY)
|
||||
navigateNext(accountUuid)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError(error: AccountCreatorResult.Error) {
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
error = error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeNavigateBack() {
|
||||
if (!state.value.isLoading) {
|
||||
navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
viewModelScope.coroutineContext.cancelChildren()
|
||||
emitEffect(Effect.NavigateBack)
|
||||
}
|
||||
|
||||
private fun navigateNext(accountUuid: AccountUuid) {
|
||||
viewModelScope.coroutineContext.cancelChildren()
|
||||
emitEffect(Effect.NavigateNext(accountUuid))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.display
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
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.layout.requiredHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextLabelSmall
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.TextInput
|
||||
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import app.k9mail.feature.account.common.ui.AppTitleTopHeader
|
||||
import app.k9mail.feature.account.common.ui.item.defaultHeadlineItemPadding
|
||||
import app.k9mail.feature.account.common.ui.item.defaultItemPadding
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.State
|
||||
import net.thunderbird.core.ui.compose.common.modifier.testTagAsResourceId
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun DisplayOptionsContent(
|
||||
state: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
brandName: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
|
||||
ResponsiveWidthContainer(
|
||||
modifier = Modifier
|
||||
.testTagAsResourceId("DisplayOptionsContent")
|
||||
.consumeWindowInsets(contentPadding)
|
||||
.padding(contentPadding)
|
||||
.then(modifier),
|
||||
) { contentPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding(),
|
||||
contentPadding = contentPadding,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
|
||||
) {
|
||||
item {
|
||||
AppTitleTopHeader(
|
||||
title = brandName,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextLabelSmall(
|
||||
text = stringResource(id = R.string.account_setup_options_section_display_options),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(defaultHeadlineItemPadding()),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextInput(
|
||||
text = state.accountName.value,
|
||||
errorMessage = state.accountName.error?.toResourceString(resources),
|
||||
onTextChange = { onEvent(Event.OnAccountNameChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_options_account_name_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
modifier = Modifier.testTagAsResourceId("account_setup_display_options_account_name_input"),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextInput(
|
||||
text = state.displayName.value,
|
||||
errorMessage = state.displayName.error?.toResourceString(resources),
|
||||
onTextChange = { onEvent(Event.OnDisplayNameChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_options_display_name_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
isRequired = true,
|
||||
modifier = Modifier.testTagAsResourceId("account_setup_display_options_display_name_input"),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextInput(
|
||||
text = state.emailSignature.value,
|
||||
errorMessage = state.emailSignature.error?.toResourceString(resources),
|
||||
onTextChange = { onEvent(Event.OnEmailSignatureChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_options_email_signature_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
isSingleLine = false,
|
||||
modifier = Modifier.testTagAsResourceId("account_setup_display_options_signature_input"),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.requiredHeight(MainTheme.sizes.smaller))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.display
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
|
||||
import app.k9mail.feature.account.common.domain.input.StringInputField
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
interface DisplayOptionsContract {
|
||||
|
||||
interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
|
||||
|
||||
data class State(
|
||||
val accountName: StringInputField = StringInputField(),
|
||||
val displayName: StringInputField = StringInputField(),
|
||||
val emailSignature: StringInputField = StringInputField(),
|
||||
)
|
||||
|
||||
sealed interface Event {
|
||||
data class OnAccountNameChanged(val accountName: String) : Event
|
||||
data class OnDisplayNameChanged(val displayName: String) : Event
|
||||
data class OnEmailSignatureChanged(val emailSignature: String) : Event
|
||||
|
||||
data object LoadAccountState : Event
|
||||
|
||||
data object OnNextClicked : Event
|
||||
data object OnBackClicked : Event
|
||||
}
|
||||
|
||||
sealed interface Effect {
|
||||
data object NavigateNext : Effect
|
||||
data object NavigateBack : Effect
|
||||
}
|
||||
|
||||
interface Validator {
|
||||
fun validateAccountName(accountName: String): ValidationResult
|
||||
fun validateDisplayName(displayName: String): ValidationResult
|
||||
fun validateEmailSignature(emailSignature: String): ValidationResult
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.display
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.k9mail.core.ui.compose.common.mvi.observe
|
||||
import app.k9mail.core.ui.compose.designsystem.template.Scaffold
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBar
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.ViewModel
|
||||
import net.thunderbird.core.common.provider.BrandNameProvider
|
||||
|
||||
@Composable
|
||||
internal fun DisplayOptionsScreen(
|
||||
onNext: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ViewModel,
|
||||
brandNameProvider: BrandNameProvider,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (state, dispatch) = viewModel.observe { effect ->
|
||||
when (effect) {
|
||||
Effect.NavigateBack -> onBack()
|
||||
Effect.NavigateNext -> onNext()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
dispatch(Event.LoadAccountState)
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
dispatch(Event.OnBackClicked)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
WizardNavigationBar(
|
||||
onNextClick = { dispatch(Event.OnNextClicked) },
|
||||
onBackClick = { dispatch(Event.OnBackClicked) },
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) { innerPadding ->
|
||||
DisplayOptionsContent(
|
||||
state = state.value,
|
||||
onEvent = { dispatch(it) },
|
||||
contentPadding = innerPadding,
|
||||
brandName = brandNameProvider.brandName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.display
|
||||
|
||||
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.input.StringInputField
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.State
|
||||
|
||||
internal fun AccountState.toDisplayOptionsState(): State {
|
||||
val options = displayOptions
|
||||
return if (options == null) {
|
||||
State(
|
||||
accountName = StringInputField(emailAddress ?: ""),
|
||||
// displayName = StringInputField(""),
|
||||
// TODO: get display name from: preferences.defaultAccount?.senderName ?: ""
|
||||
)
|
||||
} else {
|
||||
State(
|
||||
accountName = StringInputField(options.accountName),
|
||||
displayName = StringInputField(options.displayName),
|
||||
emailSignature = StringInputField(options.emailSignature ?: ""),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun State.toAccountDisplayOptions(): AccountDisplayOptions {
|
||||
return AccountDisplayOptions(
|
||||
accountName = accountName.value,
|
||||
displayName = displayName.value,
|
||||
emailSignature = emailSignature.value.takeIf { it.isNotEmpty() },
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.display
|
||||
|
||||
import android.content.res.Resources
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateAccountName.ValidateAccountNameError
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateAccountName.ValidateAccountNameError.BlankAccountName
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateDisplayName.ValidateDisplayNameError
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateDisplayName.ValidateDisplayNameError.EmptyDisplayName
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailSignature.ValidateEmailSignatureError
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailSignature.ValidateEmailSignatureError.BlankEmailSignature
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationError
|
||||
|
||||
internal fun ValidationError.toResourceString(resources: Resources): String {
|
||||
return when (this) {
|
||||
is ValidateAccountNameError -> toAccountNameErrorString(resources)
|
||||
is ValidateDisplayNameError -> toDisplayNameErrorString(resources)
|
||||
is ValidateEmailSignatureError -> toEmailSignatureErrorString(resources)
|
||||
else -> throw IllegalArgumentException("Unknown error: $this")
|
||||
}
|
||||
}
|
||||
|
||||
private fun ValidateAccountNameError.toAccountNameErrorString(resources: Resources): String {
|
||||
return when (this) {
|
||||
is BlankAccountName -> resources.getString(R.string.account_setup_options_account_name_error_blank)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ValidateDisplayNameError.toDisplayNameErrorString(resources: Resources): String {
|
||||
return when (this) {
|
||||
is EmptyDisplayName -> resources.getString(R.string.account_setup_options_display_name_error_required)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ValidateEmailSignatureError.toEmailSignatureErrorString(resources: Resources): String {
|
||||
return when (this) {
|
||||
is BlankEmailSignature -> resources.getString(R.string.account_setup_options_email_signature_error_blank)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.display
|
||||
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateAccountName
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateDisplayName
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailSignature
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.Validator
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
internal class DisplayOptionsValidator(
|
||||
private val accountNameValidator: ValidateAccountName = ValidateAccountName(),
|
||||
private val displayNameValidator: ValidateDisplayName = ValidateDisplayName(),
|
||||
private val emailSignatureValidator: ValidateEmailSignature = ValidateEmailSignature(),
|
||||
) : Validator {
|
||||
override fun validateAccountName(accountName: String): ValidationResult {
|
||||
return accountNameValidator.execute(accountName)
|
||||
}
|
||||
|
||||
override fun validateDisplayName(displayName: String): ValidationResult {
|
||||
return displayNameValidator.execute(displayName)
|
||||
}
|
||||
|
||||
override fun validateEmailSignature(emailSignature: String): ValidationResult {
|
||||
return emailSignatureValidator.execute(emailSignature)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.display
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.common.domain.AccountDomainContract
|
||||
import app.k9mail.feature.account.common.domain.input.StringInputField
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.State
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.Validator
|
||||
import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.ViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
|
||||
internal class DisplayOptionsViewModel(
|
||||
private val validator: Validator,
|
||||
private val accountStateRepository: AccountDomainContract.AccountStateRepository,
|
||||
private val accountOwnerNameProvider: AccountSetupExternalContract.AccountOwnerNameProvider,
|
||||
initialState: State? = null,
|
||||
) : BaseViewModel<State, Event, Effect>(
|
||||
initialState = initialState ?: accountStateRepository.getState().toDisplayOptionsState(),
|
||||
),
|
||||
ViewModel {
|
||||
|
||||
override fun event(event: Event) {
|
||||
when (event) {
|
||||
Event.LoadAccountState -> handleOneTimeEvent(event, ::loadAccountState)
|
||||
|
||||
is Event.OnAccountNameChanged -> updateState { state ->
|
||||
state.copy(
|
||||
accountName = state.accountName.updateValue(event.accountName),
|
||||
)
|
||||
}
|
||||
|
||||
is Event.OnDisplayNameChanged -> updateState {
|
||||
it.copy(
|
||||
displayName = it.displayName.updateValue(event.displayName),
|
||||
)
|
||||
}
|
||||
|
||||
is Event.OnEmailSignatureChanged -> updateState {
|
||||
it.copy(
|
||||
emailSignature = it.emailSignature.updateValue(event.emailSignature),
|
||||
)
|
||||
}
|
||||
|
||||
Event.OnNextClicked -> submit()
|
||||
Event.OnBackClicked -> navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAccountState() {
|
||||
viewModelScope.launch {
|
||||
val ownerName = accountOwnerNameProvider.getOwnerName().orEmpty()
|
||||
|
||||
updateState {
|
||||
val displayOptionsState = accountStateRepository.getState().toDisplayOptionsState()
|
||||
if (displayOptionsState.displayName.value.isEmpty()) {
|
||||
displayOptionsState.copy(
|
||||
displayName = StringInputField(value = ownerName),
|
||||
)
|
||||
} else {
|
||||
displayOptionsState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() = with(state.value) {
|
||||
val accountNameResult = validator.validateAccountName(accountName.value)
|
||||
val displayNameResult = validator.validateDisplayName(displayName.value)
|
||||
val emailSignatureResult = validator.validateEmailSignature(emailSignature.value)
|
||||
|
||||
val hasError = listOf(
|
||||
accountNameResult,
|
||||
displayNameResult,
|
||||
emailSignatureResult,
|
||||
).any { it is ValidationResult.Failure }
|
||||
|
||||
updateState {
|
||||
it.copy(
|
||||
accountName = it.accountName.updateFromValidationResult(accountNameResult),
|
||||
displayName = it.displayName.updateFromValidationResult(displayNameResult),
|
||||
emailSignature = it.emailSignature.updateFromValidationResult(emailSignatureResult),
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasError) {
|
||||
accountStateRepository.setDisplayOptions(state.value.toAccountDisplayOptions())
|
||||
navigateNext()
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateBack() = emitEffect(Effect.NavigateBack)
|
||||
|
||||
private fun navigateNext() = emitEffect(Effect.NavigateNext)
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.sync
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
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.layout.requiredHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextLabelSmall
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.SelectInput
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.SwitchInput
|
||||
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import app.k9mail.feature.account.common.ui.AppTitleTopHeader
|
||||
import app.k9mail.feature.account.common.ui.item.defaultHeadlineItemPadding
|
||||
import app.k9mail.feature.account.common.ui.item.defaultItemPadding
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.domain.entity.EmailCheckFrequency
|
||||
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.State
|
||||
import net.thunderbird.core.ui.compose.common.modifier.testTagAsResourceId
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun SyncOptionsContent(
|
||||
state: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
brandName: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
|
||||
ResponsiveWidthContainer(
|
||||
modifier = Modifier
|
||||
.testTagAsResourceId("SyncOptionsContent")
|
||||
.consumeWindowInsets(contentPadding)
|
||||
.padding(contentPadding)
|
||||
.then(modifier),
|
||||
) { contentPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding(),
|
||||
contentPadding = contentPadding,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
|
||||
) {
|
||||
item {
|
||||
AppTitleTopHeader(
|
||||
title = brandName,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextLabelSmall(
|
||||
text = stringResource(id = R.string.account_setup_options_section_sync_options),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(defaultHeadlineItemPadding()),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SelectInput(
|
||||
options = EmailCheckFrequency.all(),
|
||||
optionToStringTransformation = { it.toResourceString(resources) },
|
||||
selectedOption = state.checkFrequency,
|
||||
onOptionChange = { onEvent(Event.OnCheckFrequencyChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_options_account_check_frequency_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SelectInput(
|
||||
options = EmailDisplayCount.all(),
|
||||
optionToStringTransformation = { it.toResourceString(resources) },
|
||||
selectedOption = state.messageDisplayCount,
|
||||
onOptionChange = { onEvent(Event.OnMessageDisplayCountChanged(it)) },
|
||||
label = stringResource(id = R.string.account_setup_options_email_display_count_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchInput(
|
||||
text = stringResource(id = R.string.account_setup_options_show_notifications_label),
|
||||
checked = state.showNotification,
|
||||
onCheckedChange = { onEvent(Event.OnShowNotificationChanged(it)) },
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.requiredHeight(MainTheme.sizes.smaller))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.sync
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
|
||||
import app.k9mail.feature.account.setup.domain.entity.EmailCheckFrequency
|
||||
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
|
||||
|
||||
interface SyncOptionsContract {
|
||||
|
||||
interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
|
||||
|
||||
data class State(
|
||||
val checkFrequency: EmailCheckFrequency = EmailCheckFrequency.DEFAULT,
|
||||
val messageDisplayCount: EmailDisplayCount = EmailDisplayCount.DEFAULT,
|
||||
val showNotification: Boolean = true,
|
||||
)
|
||||
|
||||
sealed interface Event {
|
||||
data class OnCheckFrequencyChanged(val checkFrequency: EmailCheckFrequency) : Event
|
||||
data class OnMessageDisplayCountChanged(val messageDisplayCount: EmailDisplayCount) : Event
|
||||
data class OnShowNotificationChanged(val showNotification: Boolean) : Event
|
||||
|
||||
data object LoadAccountState : Event
|
||||
|
||||
data object OnNextClicked : Event
|
||||
data object OnBackClicked : Event
|
||||
}
|
||||
|
||||
sealed interface Effect {
|
||||
object NavigateNext : Effect
|
||||
object NavigateBack : Effect
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.sync
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.k9mail.core.ui.compose.common.mvi.observe
|
||||
import app.k9mail.core.ui.compose.designsystem.template.Scaffold
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBar
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.ViewModel
|
||||
import net.thunderbird.core.common.provider.BrandNameProvider
|
||||
|
||||
@Composable
|
||||
internal fun SyncOptionsScreen(
|
||||
onNext: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ViewModel,
|
||||
brandNameProvider: BrandNameProvider,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (state, dispatch) = viewModel.observe { effect ->
|
||||
when (effect) {
|
||||
Effect.NavigateBack -> onBack()
|
||||
Effect.NavigateNext -> onNext()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
dispatch(Event.LoadAccountState)
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
dispatch(Event.OnBackClicked)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
WizardNavigationBar(
|
||||
onNextClick = { dispatch(Event.OnNextClicked) },
|
||||
onBackClick = { dispatch(Event.OnBackClicked) },
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) { innerPadding ->
|
||||
SyncOptionsContent(
|
||||
state = state.value,
|
||||
onEvent = { dispatch(it) },
|
||||
contentPadding = innerPadding,
|
||||
brandName = brandNameProvider.brandName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.sync
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountState
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountSyncOptions
|
||||
import app.k9mail.feature.account.setup.domain.entity.EmailCheckFrequency
|
||||
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.State
|
||||
|
||||
internal fun AccountState.toSyncOptionsState(): State {
|
||||
val options = syncOptions
|
||||
return if (options == null) {
|
||||
State()
|
||||
} else {
|
||||
State(
|
||||
checkFrequency = EmailCheckFrequency.fromMinutes(options.checkFrequencyInMinutes),
|
||||
messageDisplayCount = EmailDisplayCount.fromCount(options.messageDisplayCount),
|
||||
showNotification = options.showNotification,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun State.toAccountSyncOptions(): AccountSyncOptions {
|
||||
return AccountSyncOptions(
|
||||
checkFrequencyInMinutes = checkFrequency.minutes,
|
||||
messageDisplayCount = messageDisplayCount.count,
|
||||
showNotification = showNotification,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.sync
|
||||
|
||||
import android.content.res.Resources
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.domain.entity.EmailCheckFrequency
|
||||
import app.k9mail.feature.account.setup.domain.entity.EmailDisplayCount
|
||||
|
||||
internal fun EmailDisplayCount.toResourceString(resources: Resources) = resources.getQuantityString(
|
||||
R.plurals.account_setup_options_email_display_count_messages,
|
||||
count,
|
||||
count,
|
||||
)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal fun EmailCheckFrequency.toResourceString(resources: Resources): String {
|
||||
return when (minutes) {
|
||||
-1 -> resources.getString(R.string.account_setup_options_email_check_frequency_never)
|
||||
|
||||
in 1..59 -> resources.getQuantityString(
|
||||
R.plurals.account_setup_options_email_check_frequency_minutes,
|
||||
minutes,
|
||||
minutes,
|
||||
)
|
||||
|
||||
else -> resources.getQuantityString(
|
||||
R.plurals.account_setup_options_email_check_frequency_hours,
|
||||
(minutes / 60),
|
||||
(minutes / 60),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package app.k9mail.feature.account.setup.ui.options.sync
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.common.domain.AccountDomainContract
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.State
|
||||
import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.ViewModel
|
||||
|
||||
internal class SyncOptionsViewModel(
|
||||
private val accountStateRepository: AccountDomainContract.AccountStateRepository,
|
||||
initialState: State? = null,
|
||||
) : BaseViewModel<State, Event, Effect>(
|
||||
initialState = initialState ?: accountStateRepository.getState().toSyncOptionsState(),
|
||||
),
|
||||
ViewModel {
|
||||
|
||||
override fun event(event: Event) {
|
||||
when (event) {
|
||||
Event.LoadAccountState -> handleOneTimeEvent(event, ::loadAccountState)
|
||||
|
||||
is Event.OnCheckFrequencyChanged -> updateState {
|
||||
it.copy(
|
||||
checkFrequency = event.checkFrequency,
|
||||
)
|
||||
}
|
||||
|
||||
is Event.OnMessageDisplayCountChanged -> updateState { state ->
|
||||
state.copy(
|
||||
messageDisplayCount = event.messageDisplayCount,
|
||||
)
|
||||
}
|
||||
|
||||
is Event.OnShowNotificationChanged -> updateState { state ->
|
||||
state.copy(
|
||||
showNotification = event.showNotification,
|
||||
)
|
||||
}
|
||||
|
||||
Event.OnNextClicked -> submit()
|
||||
Event.OnBackClicked -> navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAccountState() {
|
||||
updateState {
|
||||
accountStateRepository.getState().toSyncOptionsState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
accountStateRepository.setSyncOptions(state.value.toAccountSyncOptions())
|
||||
navigateNext()
|
||||
}
|
||||
|
||||
private fun navigateBack() = emitEffect(Effect.NavigateBack)
|
||||
|
||||
private fun navigateNext() = emitEffect(Effect.NavigateNext)
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package app.k9mail.feature.account.setup.ui.specialfolders
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.molecule.ContentLoadingErrorView
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.ErrorView
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.LoadingView
|
||||
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import app.k9mail.feature.account.common.ui.AppTitleTopHeader
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.State
|
||||
import net.thunderbird.core.ui.compose.common.modifier.testTagAsResourceId
|
||||
import app.k9mail.feature.account.common.R as CommonR
|
||||
|
||||
@Composable
|
||||
fun SpecialFoldersContent(
|
||||
state: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
brandName: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ResponsiveWidthContainer(
|
||||
modifier = Modifier
|
||||
.testTagAsResourceId("SpecialFoldersContent")
|
||||
.padding(contentPadding)
|
||||
.then(modifier),
|
||||
) { contentPadding ->
|
||||
Column(Modifier.padding(contentPadding)) {
|
||||
AppTitleTopHeader(
|
||||
title = brandName,
|
||||
)
|
||||
|
||||
ContentLoadingErrorView(
|
||||
state = state,
|
||||
loading = {
|
||||
LoadingView(
|
||||
message = stringResource(id = R.string.account_setup_special_folders_loading_message),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
error = { error ->
|
||||
SpecialFoldersErrorView(
|
||||
failure = error,
|
||||
onRetry = { onEvent(Event.OnRetryClicked) },
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) { state ->
|
||||
if (state.isSuccess) {
|
||||
LoadingView(
|
||||
message = stringResource(id = R.string.account_setup_special_folders_success_message),
|
||||
modifier = Modifier.padding(horizontal = MainTheme.spacings.double),
|
||||
)
|
||||
} else {
|
||||
SpecialFoldersFormContent(
|
||||
state = state.formState,
|
||||
onEvent = onEvent,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SpecialFoldersErrorView(
|
||||
failure: SpecialFoldersContract.Failure,
|
||||
onRetry: () -> Unit,
|
||||
) {
|
||||
val message = when (failure) {
|
||||
is SpecialFoldersContract.Failure.LoadFoldersFailed -> {
|
||||
failure.messageFromServer?.let { messageFromServer ->
|
||||
stringResource(id = CommonR.string.account_common_error_server_message, messageFromServer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ErrorView(
|
||||
title = stringResource(id = R.string.account_setup_special_folders_error_message),
|
||||
message = message,
|
||||
onRetry = onRetry,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(MainTheme.spacings.double),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package app.k9mail.feature.account.setup.ui.specialfolders
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.LoadingErrorState
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderOption
|
||||
|
||||
interface SpecialFoldersContract {
|
||||
|
||||
interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
|
||||
|
||||
interface FormUiModel {
|
||||
fun event(event: FormEvent, formState: FormState): FormState
|
||||
}
|
||||
|
||||
data class State(
|
||||
val formState: FormState = FormState(),
|
||||
|
||||
val isManualSetup: Boolean = false,
|
||||
val isSuccess: Boolean = false,
|
||||
override val error: Failure? = null,
|
||||
override val isLoading: Boolean = true,
|
||||
) : LoadingErrorState<Failure>
|
||||
|
||||
data class FormState(
|
||||
val archiveSpecialFolderOptions: List<SpecialFolderOption> = emptyList(),
|
||||
val draftsSpecialFolderOptions: List<SpecialFolderOption> = emptyList(),
|
||||
val sentSpecialFolderOptions: List<SpecialFolderOption> = emptyList(),
|
||||
val spamSpecialFolderOptions: List<SpecialFolderOption> = emptyList(),
|
||||
val trashSpecialFolderOptions: List<SpecialFolderOption> = emptyList(),
|
||||
|
||||
val selectedArchiveSpecialFolderOption: SpecialFolderOption = SpecialFolderOption.None(true),
|
||||
val selectedDraftsSpecialFolderOption: SpecialFolderOption = SpecialFolderOption.None(true),
|
||||
val selectedSentSpecialFolderOption: SpecialFolderOption = SpecialFolderOption.None(true),
|
||||
val selectedSpamSpecialFolderOption: SpecialFolderOption = SpecialFolderOption.None(true),
|
||||
val selectedTrashSpecialFolderOption: SpecialFolderOption = SpecialFolderOption.None(true),
|
||||
)
|
||||
|
||||
sealed interface Event {
|
||||
data object LoadSpecialFolderOptions : Event
|
||||
data object OnRetryClicked : Event
|
||||
data object OnNextClicked : Event
|
||||
data object OnBackClicked : Event
|
||||
}
|
||||
|
||||
sealed interface FormEvent : Event {
|
||||
data class ArchiveFolderChanged(val specialFolderOption: SpecialFolderOption) : FormEvent
|
||||
data class DraftsFolderChanged(val specialFolderOption: SpecialFolderOption) : FormEvent
|
||||
data class SentFolderChanged(val specialFolderOption: SpecialFolderOption) : FormEvent
|
||||
data class SpamFolderChanged(val specialFolderOption: SpecialFolderOption) : FormEvent
|
||||
data class TrashFolderChanged(val specialFolderOption: SpecialFolderOption) : FormEvent
|
||||
}
|
||||
|
||||
sealed interface Effect {
|
||||
data class NavigateNext(
|
||||
val isManualSetup: Boolean,
|
||||
) : Effect
|
||||
|
||||
data object NavigateBack : Effect
|
||||
}
|
||||
|
||||
sealed interface Failure {
|
||||
data class LoadFoldersFailed(val messageFromServer: String?) : Failure
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package app.k9mail.feature.account.setup.ui.specialfolders
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyLarge
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodySmall
|
||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.SelectInput
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import app.k9mail.feature.account.common.ui.item.defaultItemPadding
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.FormEvent
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.FormState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun SpecialFoldersFormContent(
|
||||
state: FormState,
|
||||
onEvent: (FormEvent) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.then(modifier),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.double),
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.requiredHeight(MainTheme.sizes.smaller))
|
||||
}
|
||||
|
||||
item {
|
||||
TextBodyLarge(
|
||||
text = stringResource(id = R.string.account_setup_special_folders_form_description),
|
||||
modifier = Modifier.padding(defaultItemPadding()),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SelectInput(
|
||||
options = state.archiveSpecialFolderOptions.toImmutableList(),
|
||||
selectedOption = state.selectedArchiveSpecialFolderOption,
|
||||
onOptionChange = { onEvent(FormEvent.ArchiveFolderChanged(it)) },
|
||||
optionToStringTransformation = { it.toResourceString(resources) },
|
||||
label = stringResource(R.string.account_setup_special_folders_archive_folder_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SelectInput(
|
||||
options = state.draftsSpecialFolderOptions.toImmutableList(),
|
||||
selectedOption = state.selectedDraftsSpecialFolderOption,
|
||||
onOptionChange = { onEvent(FormEvent.DraftsFolderChanged(it)) },
|
||||
optionToStringTransformation = { it.toResourceString(resources) },
|
||||
label = stringResource(id = R.string.account_setup_special_folders_drafts_folder_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SelectInput(
|
||||
options = state.sentSpecialFolderOptions.toImmutableList(),
|
||||
selectedOption = state.selectedSentSpecialFolderOption,
|
||||
onOptionChange = { onEvent(FormEvent.SentFolderChanged(it)) },
|
||||
optionToStringTransformation = { it.toResourceString(resources) },
|
||||
label = stringResource(id = R.string.account_setup_special_folders_sent_folder_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SelectInput(
|
||||
options = state.spamSpecialFolderOptions.toImmutableList(),
|
||||
selectedOption = state.selectedSpamSpecialFolderOption,
|
||||
onOptionChange = { onEvent(FormEvent.SpamFolderChanged(it)) },
|
||||
optionToStringTransformation = { it.toResourceString(resources) },
|
||||
label = stringResource(id = R.string.account_setup_special_folders_spam_folder_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SelectInput(
|
||||
options = state.trashSpecialFolderOptions.toImmutableList(),
|
||||
selectedOption = state.selectedTrashSpecialFolderOption,
|
||||
onOptionChange = { onEvent(FormEvent.TrashFolderChanged(it)) },
|
||||
optionToStringTransformation = { it.toResourceString(resources) },
|
||||
label = stringResource(id = R.string.account_setup_special_folders_trash_folder_label),
|
||||
contentPadding = defaultItemPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextBodySmall(
|
||||
text = stringResource(id = R.string.account_setup_special_folders_form_description_automatic),
|
||||
modifier = Modifier.padding(defaultItemPadding()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package app.k9mail.feature.account.setup.ui.specialfolders
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderOptions
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.FormState
|
||||
|
||||
fun SpecialFolderOptions.toFormState(): FormState {
|
||||
return FormState(
|
||||
archiveSpecialFolderOptions = archiveSpecialFolderOptions,
|
||||
draftsSpecialFolderOptions = draftsSpecialFolderOptions,
|
||||
sentSpecialFolderOptions = sentSpecialFolderOptions,
|
||||
spamSpecialFolderOptions = spamSpecialFolderOptions,
|
||||
trashSpecialFolderOptions = trashSpecialFolderOptions,
|
||||
|
||||
selectedArchiveSpecialFolderOption = archiveSpecialFolderOptions.first(),
|
||||
selectedDraftsSpecialFolderOption = draftsSpecialFolderOptions.first(),
|
||||
selectedSentSpecialFolderOption = sentSpecialFolderOptions.first(),
|
||||
selectedSpamSpecialFolderOption = spamSpecialFolderOptions.first(),
|
||||
selectedTrashSpecialFolderOption = trashSpecialFolderOptions.first(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package app.k9mail.feature.account.setup.ui.specialfolders
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderOption
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.FormEvent
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.FormState
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.FormUiModel
|
||||
|
||||
class SpecialFoldersFormUiModel : FormUiModel {
|
||||
|
||||
override fun event(event: FormEvent, formState: FormState): FormState {
|
||||
return when (event) {
|
||||
is FormEvent.ArchiveFolderChanged -> onArchiveFolderChanged(formState, event.specialFolderOption)
|
||||
is FormEvent.DraftsFolderChanged -> onDraftsFolderChanged(formState, event.specialFolderOption)
|
||||
is FormEvent.SentFolderChanged -> onSentFolderChanged(formState, event.specialFolderOption)
|
||||
is FormEvent.SpamFolderChanged -> onSpamFolderChanged(formState, event.specialFolderOption)
|
||||
is FormEvent.TrashFolderChanged -> onTrashFolderChanged(formState, event.specialFolderOption)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onArchiveFolderChanged(formState: FormState, specialFolderOption: SpecialFolderOption): FormState {
|
||||
return formState.copy(
|
||||
selectedArchiveSpecialFolderOption = specialFolderOption,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDraftsFolderChanged(formState: FormState, specialFolderOption: SpecialFolderOption): FormState {
|
||||
return formState.copy(
|
||||
selectedDraftsSpecialFolderOption = specialFolderOption,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSentFolderChanged(formState: FormState, specialFolderOption: SpecialFolderOption): FormState {
|
||||
return formState.copy(
|
||||
selectedSentSpecialFolderOption = specialFolderOption,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSpamFolderChanged(formState: FormState, specialFolderOption: SpecialFolderOption): FormState {
|
||||
return formState.copy(
|
||||
selectedSpamSpecialFolderOption = specialFolderOption,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onTrashFolderChanged(formState: FormState, specialFolderOption: SpecialFolderOption): FormState {
|
||||
return formState.copy(
|
||||
selectedTrashSpecialFolderOption = specialFolderOption,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package app.k9mail.feature.account.setup.ui.specialfolders
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.k9mail.core.ui.compose.common.mvi.observe
|
||||
import app.k9mail.core.ui.compose.designsystem.template.Scaffold
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBar
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBarState
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.ViewModel
|
||||
import net.thunderbird.core.common.provider.BrandNameProvider
|
||||
|
||||
@Composable
|
||||
fun SpecialFoldersScreen(
|
||||
onNext: (isManualSetup: Boolean) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ViewModel,
|
||||
brandNameProvider: BrandNameProvider,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (state, dispatch) = viewModel.observe { effect ->
|
||||
when (effect) {
|
||||
is Effect.NavigateNext -> onNext(effect.isManualSetup)
|
||||
Effect.NavigateBack -> onBack()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
dispatch(Event.LoadSpecialFolderOptions)
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
dispatch(Event.OnBackClicked)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
WizardNavigationBar(
|
||||
onNextClick = { dispatch(Event.OnNextClicked) },
|
||||
onBackClick = { dispatch(Event.OnBackClicked) },
|
||||
state = WizardNavigationBarState(
|
||||
showNext = state.value.isManualSetup && state.value.isLoading.not(),
|
||||
),
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) { innerPadding ->
|
||||
SpecialFoldersContent(
|
||||
state = state.value,
|
||||
onEvent = { dispatch(it) },
|
||||
contentPadding = innerPadding,
|
||||
brandName = brandNameProvider.brandName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package app.k9mail.feature.account.setup.ui.specialfolders
|
||||
|
||||
import android.content.res.Resources
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderOption
|
||||
import app.k9mail.feature.account.setup.R
|
||||
|
||||
internal fun SpecialFolderOption.toResourceString(resources: Resources) = when (this) {
|
||||
is SpecialFolderOption.None -> {
|
||||
val noneString = resources.getString(R.string.account_setup_special_folders_folder_none)
|
||||
if (isAutomatic) {
|
||||
resources.getString(R.string.account_setup_special_folders_folder_automatic, noneString)
|
||||
} else {
|
||||
noneString
|
||||
}
|
||||
}
|
||||
is SpecialFolderOption.Regular -> remoteFolder.displayName
|
||||
is SpecialFolderOption.Special -> {
|
||||
if (isAutomatic) {
|
||||
resources.getString(R.string.account_setup_special_folders_folder_automatic, remoteFolder.displayName)
|
||||
} else {
|
||||
remoteFolder.displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package app.k9mail.feature.account.setup.ui.specialfolders
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.common.domain.AccountDomainContract
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderOptions
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderSettings
|
||||
import app.k9mail.feature.account.common.ui.WizardConstants
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract.UseCase
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.FormEvent
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.State
|
||||
import app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersContract.ViewModel
|
||||
import com.fsck.k9.mail.folders.FolderFetcherException
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import net.thunderbird.core.common.domain.usecase.validation.ValidationResult
|
||||
import net.thunderbird.core.logging.legacy.Log
|
||||
|
||||
class SpecialFoldersViewModel(
|
||||
private val formUiModel: SpecialFoldersContract.FormUiModel,
|
||||
private val getSpecialFolderOptions: UseCase.GetSpecialFolderOptions,
|
||||
private val validateSpecialFolderOptions: UseCase.ValidateSpecialFolderOptions,
|
||||
private val accountStateRepository: AccountDomainContract.AccountStateRepository,
|
||||
initialState: State = State(),
|
||||
) : BaseViewModel<State, Event, Effect>(initialState),
|
||||
ViewModel {
|
||||
|
||||
override fun event(event: Event) {
|
||||
when (event) {
|
||||
Event.LoadSpecialFolderOptions -> handleOneTimeEvent(event, ::onLoadSpecialFolderOptions)
|
||||
|
||||
is FormEvent -> onFormEvent(event)
|
||||
|
||||
Event.OnNextClicked -> onNextClicked()
|
||||
Event.OnBackClicked -> onBackClicked()
|
||||
Event.OnRetryClicked -> onRetryClicked()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFormEvent(event: FormEvent) {
|
||||
updateState {
|
||||
it.copy(
|
||||
formState = formUiModel.event(event, it.formState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLoadSpecialFolderOptions() {
|
||||
viewModelScope.launch {
|
||||
val specialFolderOptions = loadSpecialFolderOptions() ?: return@launch
|
||||
|
||||
updateState { state ->
|
||||
state.copy(
|
||||
formState = specialFolderOptions.toFormState(),
|
||||
)
|
||||
}
|
||||
|
||||
val result = validateSpecialFolderOptions(specialFolderOptions)
|
||||
when (result) {
|
||||
is ValidationResult.Failure -> {
|
||||
updateState {
|
||||
it.copy(
|
||||
isManualSetup = true,
|
||||
isSuccess = false,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ValidationResult.Success -> {
|
||||
updateState {
|
||||
it.copy(
|
||||
isSuccess = true,
|
||||
)
|
||||
}
|
||||
|
||||
saveSpecialFolderSettings()
|
||||
|
||||
delay(WizardConstants.CONTINUE_NEXT_DELAY)
|
||||
navigateNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadSpecialFolderOptions(): SpecialFolderOptions? {
|
||||
return try {
|
||||
getSpecialFolderOptions()
|
||||
} catch (exception: FolderFetcherException) {
|
||||
Log.e(exception, "Error while loading special folders")
|
||||
updateState { state ->
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
isSuccess = false,
|
||||
error = SpecialFoldersContract.Failure.LoadFoldersFailed(exception.messageFromServer),
|
||||
)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSpecialFolderSettings() {
|
||||
val formState = state.value.formState
|
||||
|
||||
accountStateRepository.setSpecialFolderSettings(
|
||||
SpecialFolderSettings(
|
||||
archiveSpecialFolderOption = formState.selectedArchiveSpecialFolderOption,
|
||||
draftsSpecialFolderOption = formState.selectedDraftsSpecialFolderOption,
|
||||
sentSpecialFolderOption = formState.selectedSentSpecialFolderOption,
|
||||
spamSpecialFolderOption = formState.selectedSpamSpecialFolderOption,
|
||||
trashSpecialFolderOption = formState.selectedTrashSpecialFolderOption,
|
||||
),
|
||||
)
|
||||
updateState { state ->
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNextClicked() {
|
||||
saveSpecialFolderSettings()
|
||||
navigateNext()
|
||||
}
|
||||
|
||||
private fun navigateNext() {
|
||||
viewModelScope.coroutineContext.cancelChildren()
|
||||
emitEffect(Effect.NavigateNext(isManualSetup = state.value.isManualSetup))
|
||||
}
|
||||
|
||||
private fun onBackClicked() {
|
||||
viewModelScope.coroutineContext.cancelChildren()
|
||||
emitEffect(Effect.NavigateBack)
|
||||
}
|
||||
|
||||
private fun onRetryClicked() {
|
||||
viewModelScope.coroutineContext.cancelChildren()
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = true,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
onLoadSpecialFolderOptions()
|
||||
}
|
||||
}
|
||||
2
feature/account/setup/src/main/res/values-am/strings.xml
Normal file
2
feature/account/setup/src/main/res/values-am/strings.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
74
feature/account/setup/src/main/res/values-ar/strings.xml
Normal file
74
feature/account/setup/src/main/res/values-ar/strings.xml
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_setup_error_unknown">خطأ غير معروف</string>
|
||||
<string name="account_setup_special_folders_loading_message">يتم الآن الحصول على قائمة المجلدات…</string>
|
||||
<string name="account_setup_special_folders_drafts_folder_label">مجلد المسودات</string>
|
||||
<string name="account_setup_options_display_name_label">الاسم</string>
|
||||
<string name="account_setup_create_account_creating">جارٍ إنشاء الحساب…</string>
|
||||
<string name="account_setup_create_account_created">تم إنشاء الحساب بنجاح</string>
|
||||
<string name="account_setup_create_account_error">حدث خطأ ما أثناء محاولة إنشاء الحساب</string>
|
||||
<string name="account_setup_auto_discovery_status_header_title_configuration_found">تم العثور على الإعدادات</string>
|
||||
<string name="account_setup_auto_discovery_result_header_title_configuration_not_found">لم يتم العثور على الإعدادات</string>
|
||||
<string name="account_setup_auto_discovery_loading_message">جارٍ البحث عن الإعدادات…</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_password_required">يُرجى إدخال كلمة المرور.</string>
|
||||
<string name="account_setup_special_folders_spam_folder_label">مجلد الرسائل الغير مرغوب فيها</string>
|
||||
<string name="account_setup_special_folders_trash_folder_label">مجلد المهملات</string>
|
||||
<string name="account_setup_options_account_check_frequency_label">معدل تكرار التحقق</string>
|
||||
<string name="account_setup_options_email_check_frequency_never">مطلقًا</string>
|
||||
<string name="account_setup_options_email_display_count_label">عدد الرسائل المُراد عرضها</string>
|
||||
<string name="account_setup_options_account_name_error_blank">لا يمكن أن يكون اسم الحساب فارغًا.</string>
|
||||
<string name="account_setup_options_section_sync_options">خيارات المزامنة</string>
|
||||
<string name="account_setup_error_network">خطأ في الشبكة. يرجى التحقق من حالة الاتصال والمحاولة مرة أخرى.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_invalid">لم يتم التعرف على عنوان بريدك الإلكتروني.</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_start_tls">StartTLS</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_ssl">تشفير SSL/TLS</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_allowed">عنوان البريد الإلكتروني هذا غير مسموح به.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_supported">عنوان البريد الإلكتروني هذا غير مدعوم.</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_untrusted">هذه الإعدادات غير موثوق بها</string>
|
||||
<string name="account_setup_auto_discovery_loading_error">تعذر تحميل إعدادات البريد الإلكتروني</string>
|
||||
<string name="account_setup_auto_discovery_result_edit_configuration_button_label">تغيير الإعدادات</string>
|
||||
<string name="account_setup_special_folders_archive_folder_label">مجلد الأرشيف</string>
|
||||
<string name="account_setup_options_section_display_options">خيارات العرض</string>
|
||||
<string name="account_setup_options_account_name_label">اسم الحساب</string>
|
||||
<string name="account_setup_options_email_signature_error_blank">لا يمكن أن يكون اسم توقيع البريد الإلكتروني فارغًا.</string>
|
||||
<plurals name="account_setup_options_email_check_frequency_minutes">
|
||||
<item quantity="zero">كل 0 دقيقة</item>
|
||||
<item quantity="one">كل دقيقة</item>
|
||||
<item quantity="two">كل دقيقتين</item>
|
||||
<item quantity="few">كل %d دقائق</item>
|
||||
<item quantity="many">كل %d دقيقة</item>
|
||||
<item quantity="other">كل %d دقيقة</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_check_frequency_hours">
|
||||
<item quantity="zero">كل 0 ساعة</item>
|
||||
<item quantity="one">كل ساعة</item>
|
||||
<item quantity="two">كل ساعتين</item>
|
||||
<item quantity="few">كل %d ساعات</item>
|
||||
<item quantity="many">كل %d ساعة</item>
|
||||
<item quantity="other">كل %d ساعة</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_display_count_messages">
|
||||
<item quantity="zero">0 رسالة</item>
|
||||
<item quantity="one">رسالة واحدة</item>
|
||||
<item quantity="two">رسالتين</item>
|
||||
<item quantity="few">%d رسائل</item>
|
||||
<item quantity="many">%d رسالة</item>
|
||||
<item quantity="other">%d رسالة</item>
|
||||
</plurals>
|
||||
<string name="account_setup_auto_discovery_result_disclaimer_untrusted_configuration">لقد تلقينا الإعدادات الخاصة بخادم البريد الإلكتروني الخاص بك عبر اتصال ليس بمستوى الأمان الذي نريده. هذا يعني أن هناك فرصة ضئيلة أن يكون شخص ما قد قام بتعديلها. هل يمكنك إعادة فحص الإعدادات المزودة للتأكد من أنها كما ينبغي أن تكون؟</string>
|
||||
<string name="account_setup_special_folders_form_description_automatic">عند اختيار \"تلقائي\" سوف يتم اتباع التغييرات التي يجريها الخادم تلقائيًا بشكل مستمر. يتم عرض قيمة الخادم الحالية بين قوسين.</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_trusted">الإعداد تلقائيًا</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_not_found">الإعداد يدويًا</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_checkbox_label">أثق في هذه الإعدادات</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_error_approval_required">يتطلب ذلك الموافقة على هذه الإعدادات.</string>
|
||||
<string name="account_setup_special_folders_form_description">يُرجى تحديد المجلدات الخاصة بحسابك.</string>
|
||||
<string name="account_setup_special_folders_error_message">فشل الحصول على قائمة المجلدات من الخادم</string>
|
||||
<string name="account_setup_special_folders_success_message">لقد تم إعداد جميع المجلدات الخاصة تلقائيًا بواسطة الخادم.</string>
|
||||
<string name="account_setup_special_folders_sent_folder_label">مجلد البريد المرسَل</string>
|
||||
<string name="account_setup_special_folders_folder_none">بدون</string>
|
||||
<string name="account_setup_special_folders_folder_automatic">تلقائي (%s)</string>
|
||||
<string name="account_setup_options_email_signature_label">توقيع البريد الإلكتروني</string>
|
||||
<string name="account_setup_options_show_notifications_label">عرض الإشعارات</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_required">يُرجى إدخال عنوان البريد الإلكتروني.</string>
|
||||
<string name="account_setup_options_display_name_error_required">يُرجى ادخال الاسم.</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
2
feature/account/setup/src/main/res/values-az/strings.xml
Normal file
2
feature/account/setup/src/main/res/values-az/strings.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
5
feature/account/setup/src/main/res/values-be/strings.xml
Normal file
5
feature/account/setup/src/main/res/values-be/strings.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_setup_error_network">Памылка сеткі. Праверце стан падключэння і паспрабуйце яшчэ раз.</string>
|
||||
<string name="account_setup_error_unknown">Невядомая памылка</string>
|
||||
</resources>
|
||||
62
feature/account/setup/src/main/res/values-bg/strings.xml
Normal file
62
feature/account/setup/src/main/res/values-bg/strings.xml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_trusted">Настрой автоматично</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_untrusted">Тези настройки не са проверени</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_not_found">Ръчно настройване</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_error_approval_required">Нужно е да одобрите настройките.</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_start_tls">StartTLS</string>
|
||||
<string name="account_setup_auto_discovery_result_disclaimer_untrusted_configuration">Получихме настройките за Вашия имейл чрез интернет връзка, която не е достатъчно защитена. Това означава, че има малка вероятност някой да ги е променил. Може ли да проверите отново заредените настройки, за да се уверите че са правилни?</string>
|
||||
<string name="account_setup_auto_discovery_result_edit_configuration_button_label">Промяна на настройки</string>
|
||||
<string name="account_setup_error_unknown">Непозната грешка</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_ssl">SSL/TLS</string>
|
||||
<string name="account_setup_auto_discovery_result_header_title_configuration_not_found">Не бяха намерени настройки</string>
|
||||
<string name="account_setup_auto_discovery_status_header_title_configuration_found">Намерени са настройки</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_required">Имейл адресът е задължителен.</string>
|
||||
<string name="account_setup_auto_discovery_loading_error">Неуспешно зареждане на имейл настройки</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_checkbox_label">Одобрявам тези настройки</string>
|
||||
<string name="account_setup_error_network">Мрежова грешка. Моля, проверете състоянието на връзката си и опитайте отново.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_invalid">Имейл адресът не беше разпознат като валиден.</string>
|
||||
<string name="account_setup_auto_discovery_loading_message">Намиране на конфифурацията…</string>
|
||||
<string name="account_setup_options_account_name_error_blank">Името на акаунта не може да бъде празно.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_allowed">Този имейл адрес не е разрешен.</string>
|
||||
<string name="account_setup_options_account_check_frequency_label">Интервал на проверка</string>
|
||||
<string name="account_setup_create_account_created">Акаунтът беше създаден успешно</string>
|
||||
<string name="account_setup_options_email_signature_label">Подпис</string>
|
||||
<string name="account_setup_options_email_display_count_label">Брой съобщения, които да бъдат показани</string>
|
||||
<string name="account_setup_options_email_signature_error_blank">Името на подписа не може да бъде празно.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_supported">Този имейл адрес не се поддържа.</string>
|
||||
<string name="account_setup_create_account_creating">Създаване на акаунт…</string>
|
||||
<string name="account_setup_options_account_name_label">Име на акаунта</string>
|
||||
<string name="account_setup_options_display_name_label">Вашето име</string>
|
||||
<string name="account_setup_options_display_name_error_required">Име е задължително.</string>
|
||||
<string name="account_setup_create_account_error">Възникна грешка при създаването на акаунта</string>
|
||||
<string name="account_setup_options_email_check_frequency_never">Никога</string>
|
||||
<string name="account_setup_options_section_sync_options">Настройки за синхронизация</string>
|
||||
<string name="account_setup_options_show_notifications_label">Показване на известия</string>
|
||||
<string name="account_setup_options_section_display_options">Настройки за визуализация</string>
|
||||
<plurals name="account_setup_options_email_check_frequency_minutes">
|
||||
<item quantity="one">Всяка минута</item>
|
||||
<item quantity="other">Всяка %d минута</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_check_frequency_hours">
|
||||
<item quantity="one">Всеки час</item>
|
||||
<item quantity="other">Всеки %d час</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_display_count_messages">
|
||||
<item quantity="one">1 съобшение</item>
|
||||
<item quantity="other">%d съобщения</item>
|
||||
</plurals>
|
||||
<string name="account_setup_auto_discovery_validation_error_password_required">Паролата е задължителна.</string>
|
||||
<string name="account_setup_special_folders_form_description">Моля, посочете специалните папки за вашия профил.</string>
|
||||
<string name="account_setup_special_folders_form_description_automatic">Въведеното „Автоматично“ ще следва автоматично промените, направени от сървъра. Текущата стойност на сървъра се показва в скоби.</string>
|
||||
<string name="account_setup_special_folders_loading_message">Извличане на списъка с папки…</string>
|
||||
<string name="account_setup_special_folders_error_message">Неуспешно извличане на списъка с папки от сървъра</string>
|
||||
<string name="account_setup_special_folders_success_message">Всички специални папки са конфигурирани автоматично от сървъра.</string>
|
||||
<string name="account_setup_special_folders_archive_folder_label">папка \"Архив\"</string>
|
||||
<string name="account_setup_special_folders_drafts_folder_label">Папка \"Чернови\"</string>
|
||||
<string name="account_setup_special_folders_sent_folder_label">папка \"Изпратени\"</string>
|
||||
<string name="account_setup_special_folders_spam_folder_label">Папка \"Нежелана поща\"</string>
|
||||
<string name="account_setup_special_folders_trash_folder_label">Папка \"Кошче\"</string>
|
||||
<string name="account_setup_special_folders_folder_none">Никаква</string>
|
||||
<string name="account_setup_special_folders_folder_automatic">Автоматично (%s)</string>
|
||||
</resources>
|
||||
2
feature/account/setup/src/main/res/values-bn/strings.xml
Normal file
2
feature/account/setup/src/main/res/values-bn/strings.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
6
feature/account/setup/src/main/res/values-br/strings.xml
Normal file
6
feature/account/setup/src/main/res/values-br/strings.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_setup_error_network">Fazi rouedad. Mar plij, gwiriañ stad hor c\'hennask ha klaskit en-dro.</string>
|
||||
<string name="account_setup_error_unknown">Fazi dianav</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_required">Chomlec\'h postel rekis.</string>
|
||||
</resources>
|
||||
2
feature/account/setup/src/main/res/values-bs/strings.xml
Normal file
2
feature/account/setup/src/main/res/values-bs/strings.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
65
feature/account/setup/src/main/res/values-ca/strings.xml
Normal file
65
feature/account/setup/src/main/res/values-ca/strings.xml
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_setup_options_account_name_error_blank">El nom del compte no pot estar en blanc.</string>
|
||||
<string name="account_setup_options_account_check_frequency_label">Freqüència de comprovació</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_trusted">Configura automàticament</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_untrusted">Aquesta configuració no és fiable</string>
|
||||
<string name="account_setup_options_email_signature_label">Signatura de correu electrònic</string>
|
||||
<string name="account_setup_options_email_display_count_label">Nombre de missatges a mostrar</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_not_found">Configureu manualment</string>
|
||||
<string name="account_setup_options_email_signature_error_blank">El nom de la signatura de correu electrònic no pot estar en blanc.</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_error_approval_required">És necessari aprovar la configuració.</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_start_tls">StartTLS</string>
|
||||
<string name="account_setup_auto_discovery_result_disclaimer_untrusted_configuration">Hem rebut la configuració del vostre servidor de correu electrònic a través d\'una connexió que no és tan segura com ens agradaria. Això significa que hi ha una petita possibilitat que algú la pugui haver modificat. Podríeu comprovar la configuració proporcionada per assegurar-vos que és com hauria de ser\?</string>
|
||||
<string name="account_setup_auto_discovery_result_edit_configuration_button_label">Edita la configuració</string>
|
||||
<string name="account_setup_error_unknown">Error desconegut</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_ssl">SSL/TLS</string>
|
||||
<string name="account_setup_auto_discovery_result_header_title_configuration_not_found">No s\'ha trobat cap configuració</string>
|
||||
<string name="account_setup_options_account_name_label">Nom del compte</string>
|
||||
<string name="account_setup_auto_discovery_status_header_title_configuration_found">S\'ha trobat una configuració</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_required">Cal l\'adreça de correu electrònic.</string>
|
||||
<string name="account_setup_options_display_name_label">El teu nom</string>
|
||||
<string name="account_setup_auto_discovery_loading_error">Ha fallat la càrrega de la configuració de correu electrònic</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_checkbox_label">Confio en aquesta configuració</string>
|
||||
<string name="account_setup_options_display_name_error_required">Es requereix el teu nom.</string>
|
||||
<string name="account_setup_error_network">Error de xarxa. Comproveu l\'estat de la vostra connexió i torneu-ho a provar.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_invalid">L\'adreça de correu electrònic no es reconeix com a vàlida.</string>
|
||||
<string name="account_setup_options_email_check_frequency_never">Mai</string>
|
||||
<string name="account_setup_auto_discovery_loading_message">Cercant configuració…</string>
|
||||
<string name="account_setup_options_section_sync_options">Opcions de sincronització</string>
|
||||
<string name="account_setup_options_show_notifications_label">Mostra les notificacions</string>
|
||||
<string name="account_setup_options_section_display_options">Opcions de visualització</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_allowed">Aquesta direcció de correu electrònic no està permesa.</string>
|
||||
<string name="account_setup_create_account_created">Compte creat amb èxit</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_supported">Aquesta direcció de correu electrònic no és compatible.</string>
|
||||
<string name="account_setup_create_account_creating">S\'està creant el compte…</string>
|
||||
<string name="account_setup_create_account_error">S\'ha produït un error en intentar crear el compte</string>
|
||||
<string name="account_setup_special_folders_success_message">Totes les carpetes especials han estat configurades automàticament pel servidor.</string>
|
||||
<string name="account_setup_special_folders_folder_automatic">Automàtic (%s)</string>
|
||||
<string name="account_setup_special_folders_drafts_folder_label">Carpeta d\'esborranys</string>
|
||||
<string name="account_setup_special_folders_error_message">No s\'ha pogut obtenir la llista de carpetes del servidor</string>
|
||||
<string name="account_setup_special_folders_loading_message">S\'està obtenint la llista de carpetes…</string>
|
||||
<string name="account_setup_special_folders_form_description">Especifiqueu les carpetes especials per al vostre compte.</string>
|
||||
<string name="account_setup_special_folders_sent_folder_label">Carpeta d\'enviats</string>
|
||||
<string name="account_setup_special_folders_folder_none">Cap</string>
|
||||
<string name="account_setup_special_folders_trash_folder_label">Carpeta paperera</string>
|
||||
<string name="account_setup_special_folders_archive_folder_label">Carpeta d\'arxiu</string>
|
||||
<string name="account_setup_special_folders_spam_folder_label">Carpeta brossa</string>
|
||||
<string name="account_setup_special_folders_form_description_automatic">L\'entrada \"Automàtica\" seguirà els canvis fets pel servidor automàticament. El valor actual del servidor es mostra entre parèntesis.</string>
|
||||
<plurals name="account_setup_options_email_check_frequency_minutes">
|
||||
<item quantity="one">Cada minut</item>
|
||||
<item quantity="many">Cada %d minuts</item>
|
||||
<item quantity="other">Cada %d minuts</item>
|
||||
</plurals>
|
||||
<string name="account_setup_auto_discovery_validation_error_password_required">Es requereix contrasenya.</string>
|
||||
<plurals name="account_setup_options_email_check_frequency_hours">
|
||||
<item quantity="one">Cada hora</item>
|
||||
<item quantity="many">Cada %d hores</item>
|
||||
<item quantity="other">Cada %d hores</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_display_count_messages">
|
||||
<item quantity="one">1 missatge</item>
|
||||
<item quantity="many">%d missatges</item>
|
||||
<item quantity="other">%d missatges</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
62
feature/account/setup/src/main/res/values-co/strings.xml
Normal file
62
feature/account/setup/src/main/res/values-co/strings.xml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_required">L’indirizzu elettronicu hè richiestu.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_allowed">St’indirizzu elettronicu ùn hè micca permessu.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_invalid">St’indirizzu elettronicu ùn hè micca ricunnisciutu cum’è accettevule.</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_start_tls">StartTLS</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_ssl">SSL/TLS</string>
|
||||
<string name="account_setup_auto_discovery_loading_message">Ricerca di a cunfigurazione…</string>
|
||||
<string name="account_setup_auto_discovery_loading_error">Fiascu di u caricamentu di a cunfigurazione di u contu di messaghjeria</string>
|
||||
<string name="account_setup_auto_discovery_status_header_title_configuration_found">A cunfigurazione hè stata trova</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_untrusted">A cunfigurazione ùn hè micca degna di cunfidenza</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_checkbox_label">Facciu cunfidenza à quella cunfigurazione</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_error_approval_required">Hè richiestu d’appruvà a cunfigurazione.</string>
|
||||
<string name="account_setup_special_folders_loading_message">Riguarera di a lista di i cartulari…</string>
|
||||
<string name="account_setup_special_folders_error_message">Fiascu di a riguarera di a lista di i cartulari da u servitore</string>
|
||||
<string name="account_setup_special_folders_success_message">Tutti i cartulari speziali sò stati cunfigurati autumaticamente da u servitore.</string>
|
||||
<string name="account_setup_special_folders_archive_folder_label">Cartulare di l’archivii</string>
|
||||
<string name="account_setup_special_folders_sent_folder_label">Cartulare di i messaghji mandati</string>
|
||||
<string name="account_setup_special_folders_folder_none">Nisunu</string>
|
||||
<string name="account_setup_special_folders_folder_automatic">Autumaticu (%s)</string>
|
||||
<string name="account_setup_options_section_display_options">Ozzioni d’affissera</string>
|
||||
<string name="account_setup_options_account_name_error_blank">U nome di u contu ùn pò micca esse viotu.</string>
|
||||
<string name="account_setup_options_display_name_error_required">U vostru nome hè richiestu.</string>
|
||||
<string name="account_setup_options_email_signature_label">Segnatura di u messaghju elettronicu</string>
|
||||
<string name="account_setup_options_account_check_frequency_label">Frequenza di cuntrollu</string>
|
||||
<string name="account_setup_options_email_check_frequency_never">Mai</string>
|
||||
<plurals name="account_setup_options_email_check_frequency_minutes">
|
||||
<item quantity="one">Ogni minutu</item>
|
||||
<item quantity="other">Tutti i %d minuti</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_check_frequency_hours">
|
||||
<item quantity="one">Ogni ora</item>
|
||||
<item quantity="other">Tutte e %d ore</item>
|
||||
</plurals>
|
||||
<string name="account_setup_options_email_display_count_label">Numeru di messaghji à affissà</string>
|
||||
<string name="account_setup_options_show_notifications_label">Affissà e nutificazioni</string>
|
||||
<string name="account_setup_create_account_creating">Creazione di contu…</string>
|
||||
<string name="account_setup_create_account_error">Un sbagliu hè accadutu durante a creazione di u contu</string>
|
||||
<string name="account_setup_create_account_created">U contu hè statu creatu currettamente</string>
|
||||
<string name="account_setup_error_network">Sbagliu di a reta. Ci vole à verificà a vostra cunnessione è pruvà torna.</string>
|
||||
<string name="account_setup_error_unknown">Sbagliu scunnisciutu</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_supported">St’indirizzu elettronicu ùn hè micca accettatu.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_password_required">A parolla d’intesa hè richiesta.</string>
|
||||
<string name="account_setup_auto_discovery_result_header_title_configuration_not_found">A cunfigurazione ùn si trova micca</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_trusted">Cunfigurà autumaticamente</string>
|
||||
<string name="account_setup_special_folders_form_description">Selezziunate i cartulari speziali per u vostru contu.</string>
|
||||
<string name="account_setup_auto_discovery_result_disclaimer_untrusted_configuration">Avemu ricevutu a cunfigurazione per u vostru servitore di messaghjeria via una cunnessione chì ùn hè micca tantu sicura chì no a vuleriamu. Vole si dì chì ci hè una pussibilità chjuca chjuca chì qualchissia l’abbia alterata. Puderiate verificà torna a cunfigurazione pruvista per assicurassi ch’ella sia degna di cunfidenza ?</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_not_found">Cunfigurà manualmente</string>
|
||||
<string name="account_setup_auto_discovery_result_edit_configuration_button_label">Mudificà a cunfigurazione</string>
|
||||
<string name="account_setup_special_folders_form_description_automatic">L’ozzione « Autumaticu » seguiterà autumaticamente i cambiamenti fatti da u servitore. U valore attuale di u servitore hè affissatu trà parentesi.</string>
|
||||
<string name="account_setup_special_folders_drafts_folder_label">Cartulare di e bruttacopie</string>
|
||||
<string name="account_setup_special_folders_spam_folder_label">Cartulare di i merzaghji (dispiacevule)</string>
|
||||
<string name="account_setup_special_folders_trash_folder_label">Cartulare di a curbella</string>
|
||||
<string name="account_setup_options_account_name_label">Nome di u contu</string>
|
||||
<string name="account_setup_options_display_name_label">U vostru nome</string>
|
||||
<string name="account_setup_options_section_sync_options">Ozzioni di sincrunizazione</string>
|
||||
<string name="account_setup_options_email_signature_error_blank">A segnatura di u messaghju elettronicu ùn pò micca esse viota.</string>
|
||||
<plurals name="account_setup_options_email_display_count_messages">
|
||||
<item quantity="one">1 messaghju</item>
|
||||
<item quantity="other">%d messaghji</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
68
feature/account/setup/src/main/res/values-cs/strings.xml
Normal file
68
feature/account/setup/src/main/res/values-cs/strings.xml
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_setup_options_account_name_error_blank">Název účtu nemůže být prázdný.</string>
|
||||
<string name="account_setup_options_account_check_frequency_label">Četnost kontroly</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_trusted">Nastavit automaticky</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_untrusted">Toto nastavení není důvěryhodné</string>
|
||||
<string name="account_setup_options_email_signature_label">Podpis e-mailů</string>
|
||||
<string name="account_setup_options_email_display_count_label">Počet zobrazovaných zpráv</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_not_found">Nastavit ručně</string>
|
||||
<string name="account_setup_options_email_signature_error_blank">Název podpisu e-mailu nemůže být prázdný.</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_error_approval_required">Nastavení je potřeba schválit.</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_start_tls">StartTLS</string>
|
||||
<string name="account_setup_auto_discovery_result_disclaimer_untrusted_configuration">Nastavení pro váš e-mailový server bylo získáno prostřednictvím spojení, které není tak zabezpečené, jak bychom si přáli. Existuje malá pravděpodobnost, že ho někdo změnil. Zkontrolujte, že je nabízené nastavení v pořádku.</string>
|
||||
<string name="account_setup_auto_discovery_result_edit_configuration_button_label">Upravit nastavení</string>
|
||||
<string name="account_setup_error_unknown">Neznámá chyba</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_ssl">SSL/TLS</string>
|
||||
<string name="account_setup_auto_discovery_result_header_title_configuration_not_found">Nastavení nenalezeno</string>
|
||||
<string name="account_setup_options_account_name_label">Název účtu</string>
|
||||
<string name="account_setup_auto_discovery_status_header_title_configuration_found">Nastavení nalezeno</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_required">E-mailová adresa je vyžadována.</string>
|
||||
<string name="account_setup_options_display_name_label">Vaše jméno</string>
|
||||
<string name="account_setup_auto_discovery_loading_error">Nastavení e-mailu se nepodařilo nahrát</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_checkbox_label">Tomuto nastavení důvěřuji</string>
|
||||
<string name="account_setup_options_display_name_error_required">Vaše jméno je vyžadováno.</string>
|
||||
<string name="account_setup_error_network">Chyba sítě. Zkontrolujte prosím stav svého připojení a zkuste to znovu.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_invalid">Toto není rozeznáno jako platná e-mailová adresa.</string>
|
||||
<string name="account_setup_options_email_check_frequency_never">Nikdy</string>
|
||||
<string name="account_setup_auto_discovery_loading_message">Zjišťování konfigurace…</string>
|
||||
<string name="account_setup_options_section_sync_options">Nastavení synchronizace</string>
|
||||
<string name="account_setup_options_show_notifications_label">Zobrazovat oznámení</string>
|
||||
<string name="account_setup_options_section_display_options">Nastavení zobrazení</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_allowed">Tato e-mailová adresa není dovolena.</string>
|
||||
<string name="account_setup_create_account_created">Účet úspěšně vytvořen</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_supported">Tato e-mailová adresa není podporována.</string>
|
||||
<string name="account_setup_create_account_creating">Vytváření účtu…</string>
|
||||
<string name="account_setup_create_account_error">Při vytváření účtu nastala chyba</string>
|
||||
<string name="account_setup_special_folders_form_description">Zvolte prosím speciální složky pro svůj účet.</string>
|
||||
<string name="account_setup_special_folders_form_description_automatic">Volba \"Automaticky\" sleduje změny provedené na serveru. Aktuální hodnota je zobrazena v závorkách.</string>
|
||||
<string name="account_setup_special_folders_loading_message">Získávání seznamu složek…</string>
|
||||
<string name="account_setup_special_folders_error_message">Nepodařilo se získat seznam složek ze serveru</string>
|
||||
<string name="account_setup_special_folders_success_message">Všechny speciální složky byly nastaveny automaticky na serveru.</string>
|
||||
<string name="account_setup_special_folders_archive_folder_label">Archiv</string>
|
||||
<string name="account_setup_special_folders_drafts_folder_label">Koncepty</string>
|
||||
<string name="account_setup_special_folders_sent_folder_label">Odeslané</string>
|
||||
<string name="account_setup_special_folders_spam_folder_label">Spam</string>
|
||||
<string name="account_setup_special_folders_trash_folder_label">Koš</string>
|
||||
<string name="account_setup_special_folders_folder_none">Žádný</string>
|
||||
<string name="account_setup_special_folders_folder_automatic">Automaticky (%s)</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_password_required">Heslo je vyžadováno.</string>
|
||||
<plurals name="account_setup_options_email_check_frequency_minutes">
|
||||
<item quantity="one">Každou minutu</item>
|
||||
<item quantity="few">Každé %d minuty</item>
|
||||
<item quantity="many">Každých %d minut</item>
|
||||
<item quantity="other">Každých %d minut</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_check_frequency_hours">
|
||||
<item quantity="one">Každou hodinu</item>
|
||||
<item quantity="few">Každé %d hodiny</item>
|
||||
<item quantity="many">Každých %d hodin</item>
|
||||
<item quantity="other">Každých %d hodin</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_display_count_messages">
|
||||
<item quantity="one">1 zpráva</item>
|
||||
<item quantity="few">%d zprávy</item>
|
||||
<item quantity="many">%d zpráv</item>
|
||||
<item quantity="other">%d zpráv</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
2
feature/account/setup/src/main/res/values-cy/strings.xml
Normal file
2
feature/account/setup/src/main/res/values-cy/strings.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
22
feature/account/setup/src/main/res/values-da/strings.xml
Normal file
22
feature/account/setup/src/main/res/values-da/strings.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_trusted">Konfigurér automatisk</string>
|
||||
<string name="account_setup_options_email_signature_label">Email signatur</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_not_found">Konfigurér manuelt</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_start_tls">StartTLS</string>
|
||||
<string name="account_setup_auto_discovery_result_edit_configuration_button_label">Redigér konfiguration</string>
|
||||
<string name="account_setup_error_unknown">Ukendt fejl</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_ssl">SSL/TLS</string>
|
||||
<string name="account_setup_auto_discovery_result_header_title_configuration_not_found">Konfiguration ikke fundet</string>
|
||||
<string name="account_setup_options_account_name_label">Konto navn</string>
|
||||
<string name="account_setup_auto_discovery_status_header_title_configuration_found">Konfiguration fundet</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_required">Email adresse er påkrævet.</string>
|
||||
<string name="account_setup_options_display_name_label">Vist navn</string>
|
||||
<string name="account_setup_options_display_name_error_required">Vist navn er påkrævet.</string>
|
||||
<string name="account_setup_error_network">Netværk</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_invalid">Email adresse er ugyldig.</string>
|
||||
<string name="account_setup_options_email_check_frequency_never">Aldrig</string>
|
||||
<string name="account_setup_options_section_sync_options">Synkroniserings indstillinger</string>
|
||||
<string name="account_setup_options_show_notifications_label">Vis notifikationer</string>
|
||||
<string name="account_setup_options_section_display_options">Visningsindstillinger</string>
|
||||
</resources>
|
||||
62
feature/account/setup/src/main/res/values-de/strings.xml
Normal file
62
feature/account/setup/src/main/res/values-de/strings.xml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_setup_options_account_name_error_blank">Kontoname darf nicht leer sein.</string>
|
||||
<string name="account_setup_options_account_check_frequency_label">Prüfintervall</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_trusted">Automatisch konfigurieren</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_untrusted">Diese Konfiguration ist nicht vertrauenswürdig</string>
|
||||
<string name="account_setup_options_email_signature_label">E-Mail-Signatur</string>
|
||||
<string name="account_setup_options_email_display_count_label">Anzahl der anzuzeigenden Nachrichten</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_not_found">Manuell konfigurieren</string>
|
||||
<string name="account_setup_options_email_signature_error_blank">Name der E-Mail-Signatur darf nicht leer sein.</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_error_approval_required">Es ist erforderlich, die Konfiguration zu bestätigen.</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_start_tls">StartTLS</string>
|
||||
<string name="account_setup_auto_discovery_result_disclaimer_untrusted_configuration">Wir haben die Konfiguration für deinen E-Mail-Server über eine Verbindung erhalten, die nicht so sicher ist, wie es wünschenswert wäre. Das bedeutet, dass eine geringe Möglichkeit besteht, dass jemand sie verändert haben könnte. Könntest du bitte die bereitgestellte Konfiguration noch einmal überprüfen, um sicherzustellen, dass sie so ist, wie sie sein sollte\?</string>
|
||||
<string name="account_setup_auto_discovery_result_edit_configuration_button_label">Konfiguration bearbeiten</string>
|
||||
<string name="account_setup_error_unknown">Unbekannter Fehler</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_ssl">SSL/TLS</string>
|
||||
<string name="account_setup_auto_discovery_result_header_title_configuration_not_found">Konfiguration nicht gefunden</string>
|
||||
<string name="account_setup_options_account_name_label">Kontoname</string>
|
||||
<string name="account_setup_auto_discovery_status_header_title_configuration_found">Konfiguration gefunden</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_required">E-Mail-Adresse ist erforderlich.</string>
|
||||
<string name="account_setup_options_display_name_label">Dein Name</string>
|
||||
<string name="account_setup_auto_discovery_loading_error">E-Mail-Konfiguration konnte nicht geladen werden</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_checkbox_label">Ich vertraue dieser Konfiguration</string>
|
||||
<string name="account_setup_options_display_name_error_required">Dein Name ist erforderlich.</string>
|
||||
<string name="account_setup_error_network">Netzwerkfehler. Bitte überprüfe deinen Verbindungsstatus und versuche es erneut.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_invalid">Diese Adresse wird nicht als gültige E-Mail-Adresse erkannt.</string>
|
||||
<string name="account_setup_options_email_check_frequency_never">Niemals</string>
|
||||
<string name="account_setup_auto_discovery_loading_message">E-Mail-Konfiguration suchen…</string>
|
||||
<string name="account_setup_options_section_sync_options">Synchronisierungsoptionen</string>
|
||||
<string name="account_setup_options_show_notifications_label">Benachrichtigungen anzeigen</string>
|
||||
<string name="account_setup_options_section_display_options">Anzeigeoptionen</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_allowed">Diese E-Mail-Adresse ist nicht erlaubt.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_supported">Diese E-Mail-Adresse wird nicht unterstützt.</string>
|
||||
<string name="account_setup_create_account_created">Konto erfolgreich erstellt</string>
|
||||
<string name="account_setup_create_account_creating">Konto wird erstellt…</string>
|
||||
<string name="account_setup_create_account_error">Beim Versuch, das Konto zu erstellen, ist ein Fehler aufgetreten</string>
|
||||
<string name="account_setup_special_folders_success_message">Alle besonderen Ordner wurden vom Server automatisch konfiguriert.</string>
|
||||
<string name="account_setup_special_folders_folder_automatic">Automatisch (%s)</string>
|
||||
<string name="account_setup_special_folders_form_description">Bitte wähle die besonderen Ordner für dein Konto aus.</string>
|
||||
<string name="account_setup_special_folders_archive_folder_label">Archiv</string>
|
||||
<string name="account_setup_special_folders_form_description_automatic">Der Eintrag \"Automatisch\" übernimmt die vom Server vorgenommenen Änderungen. Der aktuelle Serverwert wird in Klammern angezeigt.</string>
|
||||
<string name="account_setup_special_folders_loading_message">Liste der Ordner wird abgerufen…</string>
|
||||
<string name="account_setup_special_folders_error_message">Liste der Ordner konnte nicht vom Server abgerufen werden</string>
|
||||
<string name="account_setup_special_folders_sent_folder_label">Gesendet</string>
|
||||
<string name="account_setup_special_folders_drafts_folder_label">Entwürfe</string>
|
||||
<string name="account_setup_special_folders_trash_folder_label">Papierkorb</string>
|
||||
<string name="account_setup_special_folders_folder_none">Keine</string>
|
||||
<string name="account_setup_special_folders_spam_folder_label">Spam</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_password_required">Passwort ist erforderlich.</string>
|
||||
<plurals name="account_setup_options_email_check_frequency_minutes">
|
||||
<item quantity="one">Jede Minute</item>
|
||||
<item quantity="other">Alle %d Minuten</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_check_frequency_hours">
|
||||
<item quantity="one">Jede Stunde</item>
|
||||
<item quantity="other">Alle %d Stunden</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_display_count_messages">
|
||||
<item quantity="one">1 Nachricht</item>
|
||||
<item quantity="other">%d Nachrichten</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
62
feature/account/setup/src/main/res/values-el/strings.xml
Normal file
62
feature/account/setup/src/main/res/values-el/strings.xml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_setup_options_account_name_error_blank">Το όνομα του λογαριασμού δεν μπορεί να είναι κενό.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_allowed">Αυτή η διεύθυνση email δεν επιτρέπεται.</string>
|
||||
<string name="account_setup_options_account_check_frequency_label">Συχνότητα ελέγχου</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_trusted">Αυτόματη διαμόρφωση</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_untrusted">Αυτή η διαμόρφωση δεν είναι αξιόπιστη</string>
|
||||
<string name="account_setup_create_account_created">Επιτυχής δημιουργία λογαριασμού</string>
|
||||
<string name="account_setup_options_email_signature_label">Υπογραφή email</string>
|
||||
<string name="account_setup_options_email_display_count_label">Αριθμός μηνυμάτων προς εμφάνιση</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_not_found">Χειροκίνητη διαμόρφωση</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_error_approval_required">Απαιτείται η έγκριση της διαμόρφωσης.</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_start_tls">StartTLS</string>
|
||||
<string name="account_setup_auto_discovery_result_disclaimer_untrusted_configuration">Λάβαμε τις ρυθμίσεις για τον διακομιστή ηλεκτρονικού ταχυδρομείου σας μέσω μιας σύνδεσης που δεν είναι τόσο ασφαλής όσο θα θέλαμε. Αυτό σημαίνει ότι υπάρχει μια μικρή πιθανότητα κάποιος να την έχει τροποποιήσει. Μπορείτε να ελέγξετε ξανά τις ρυθμίσεις που μας δώσατε για να βεβαιωθείτε ότι είναι όπως πρέπει;</string>
|
||||
<string name="account_setup_auto_discovery_result_edit_configuration_button_label">Επεξεργασία διαμόρφωσης</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_supported">Αυτή η διεύθυνση email δεν υποστηρίζεται.</string>
|
||||
<string name="account_setup_error_unknown">Άγνωστο σφάλμα</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_ssl">SSL/TLS</string>
|
||||
<string name="account_setup_auto_discovery_result_header_title_configuration_not_found">Η διαμόρφωση δεν βρέθηκε</string>
|
||||
<string name="account_setup_create_account_creating">Δημιουργία λογαριασμού…</string>
|
||||
<string name="account_setup_options_account_name_label">Όνομα λογαριασμού</string>
|
||||
<string name="account_setup_auto_discovery_status_header_title_configuration_found">Η διαμόρφωση βρέθηκε</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_required">Απαιτείται διεύθυνση email.</string>
|
||||
<string name="account_setup_options_display_name_label">Το όνομά σας</string>
|
||||
<string name="account_setup_auto_discovery_loading_error">Η φόρτωση της διαμόρφωσης email απέτυχε</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_checkbox_label">Εμπιστεύομαι αυτήν τη διαμόρφωση</string>
|
||||
<string name="account_setup_options_display_name_error_required">Το όνομά σας είναι απαραίτητο.</string>
|
||||
<string name="account_setup_create_account_error">Προέκυψε σφάλμα κατά την προσπάθεια δημιουργίας του λογαριασμού</string>
|
||||
<string name="account_setup_error_network">Σφάλμα δικτύου. Ελέγξτε την κατάσταση της σύνδεσής σας και προσπαθήστε ξανά.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_invalid">Η διεύθυνση email δεν αναγνωρίζεται ως έγκυρη.</string>
|
||||
<string name="account_setup_options_email_check_frequency_never">Ποτέ</string>
|
||||
<string name="account_setup_auto_discovery_loading_message">Αναζήτηση διαμόρφωσης…</string>
|
||||
<string name="account_setup_options_section_sync_options">Επιλογές συγχρονισμού</string>
|
||||
<string name="account_setup_options_show_notifications_label">Εμφάνιση ειδοποιήσεων</string>
|
||||
<string name="account_setup_options_section_display_options">Επιλογές εμφάνισης</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_password_required">Απαιτείται κωδικός πρόσβασης.</string>
|
||||
<string name="account_setup_special_folders_form_description">Καθορίστε τους ειδικούς φακέλους για τον λογαριασμό σας.</string>
|
||||
<string name="account_setup_special_folders_sent_folder_label">Φάκελος «Απεσταλμένα»</string>
|
||||
<string name="account_setup_special_folders_spam_folder_label">Φάκελος «Ανεπιθύμητα»</string>
|
||||
<string name="account_setup_special_folders_success_message">Όλοι οι ειδικοί φάκελοι έχουν ρυθμιστεί αυτόματα από τον διακομιστή.</string>
|
||||
<string name="account_setup_special_folders_folder_automatic">Αυτόματα (%s)</string>
|
||||
<string name="account_setup_special_folders_trash_folder_label">Φάκελος «Απορρίμματα»</string>
|
||||
<string name="account_setup_special_folders_folder_none">Κανένας</string>
|
||||
<string name="account_setup_special_folders_drafts_folder_label">Φάκελος «Προσχέδια»</string>
|
||||
<string name="account_setup_special_folders_archive_folder_label">Φάκελος «Αρχειοθήκη»</string>
|
||||
<string name="account_setup_special_folders_loading_message">Φόρτωση λίστας φακέλων…</string>
|
||||
<string name="account_setup_special_folders_error_message">Αποτυχία φόρτωσης της λίστας των φακέλων από τον διακομιστή</string>
|
||||
<string name="account_setup_options_email_signature_error_blank">Το όνομα της υπογραφής email δεν μπορεί να είναι κενό.</string>
|
||||
<plurals name="account_setup_options_email_check_frequency_hours">
|
||||
<item quantity="one">Κάθε ώρα</item>
|
||||
<item quantity="other">Κάθε %d ώρες</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_display_count_messages">
|
||||
<item quantity="one">1 μήνυμα</item>
|
||||
<item quantity="other">%d μηνύματα</item>
|
||||
</plurals>
|
||||
<string name="account_setup_special_folders_form_description_automatic">Η καταχώριση «Αυτόματη» θα ακολουθεί αυτόματα τις αλλαγές που πραγματοποιούνται από το διακομιστή. Η τρέχουσα τιμή του διακομιστή εμφανίζεται σε παρένθεση.</string>
|
||||
<plurals name="account_setup_options_email_check_frequency_minutes">
|
||||
<item quantity="one">Κάθε λεπτό</item>
|
||||
<item quantity="other">Κάθε %d λεπτά</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
37
feature/account/setup/src/main/res/values-en-rGB/strings.xml
Normal file
37
feature/account/setup/src/main/res/values-en-rGB/strings.xml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_setup_options_account_name_error_blank">Account name can\'t be blank.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_allowed">This email address is not allowed.</string>
|
||||
<string name="account_setup_options_account_check_frequency_label">Check frequency</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_trusted">Configure automatically</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_untrusted">This configuration is not trusted</string>
|
||||
<string name="account_setup_create_account_created">Account successfully created</string>
|
||||
<string name="account_setup_options_email_signature_label">Email signature</string>
|
||||
<string name="account_setup_options_email_display_count_label">Number of messages to display</string>
|
||||
<string name="account_setup_auto_discovery_result_header_subtitle_configuration_not_found">Configure manually</string>
|
||||
<string name="account_setup_options_email_signature_error_blank">Email signature name can\'t be blank.</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_error_approval_required">It is required to approve the configuration.</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_start_tls">StartTLS</string>
|
||||
<string name="account_setup_auto_discovery_result_disclaimer_untrusted_configuration">We received the configuration for your email server over a connection that isn\'t as secure as we\'d like. This means that there is a tiny chance that someone could have altered it. Could you please double-check the provided configuration to make sure it\'s as it should be?</string>
|
||||
<string name="account_setup_auto_discovery_result_edit_configuration_button_label">Edit configuration</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_supported">This email address is not supported.</string>
|
||||
<string name="account_setup_error_unknown">Unknown error</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_ssl">SSL/TLS</string>
|
||||
<string name="account_setup_auto_discovery_result_header_title_configuration_not_found">Configuration not found</string>
|
||||
<string name="account_setup_create_account_creating">Creating account…</string>
|
||||
<string name="account_setup_options_account_name_label">Account name</string>
|
||||
<string name="account_setup_auto_discovery_status_header_title_configuration_found">Configuration found</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_required">Email address is required.</string>
|
||||
<string name="account_setup_options_display_name_label">Display name</string>
|
||||
<string name="account_setup_auto_discovery_loading_error">Failed to load email configuration</string>
|
||||
<string name="account_setup_auto_discovery_result_approval_checkbox_label">I trust this configuration</string>
|
||||
<string name="account_setup_options_display_name_error_required">Display name is required.</string>
|
||||
<string name="account_setup_create_account_error">An error occurred while trying to create the account</string>
|
||||
<string name="account_setup_error_network">Network error. Please check your connection status and try again.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_invalid">This is not recognised as a valid email address.</string>
|
||||
<string name="account_setup_options_email_check_frequency_never">Never</string>
|
||||
<string name="account_setup_auto_discovery_loading_message">Finding email details</string>
|
||||
<string name="account_setup_options_section_sync_options">Sync options</string>
|
||||
<string name="account_setup_options_show_notifications_label">Show notifications</string>
|
||||
<string name="account_setup_options_section_display_options">Display options</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
47
feature/account/setup/src/main/res/values-eo/strings.xml
Normal file
47
feature/account/setup/src/main/res/values-eo/strings.xml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_setup_auto_discovery_validation_error_password_required">Pasvorto necesas.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_required">Retpoŝta adreso necesas.</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_start_tls">StartTLS</string>
|
||||
<string name="account_setup_error_network">Reta eraro. Bonvolu kontroli vian konektan staton kaj provi denove.</string>
|
||||
<string name="account_setup_error_unknown">Nekonata eraro</string>
|
||||
<string name="account_setup_auto_discovery_connection_security_ssl">SSL/TLS</string>
|
||||
<string name="account_setup_special_folders_sent_folder_label">Senditujo</string>
|
||||
<string name="account_setup_special_folders_spam_folder_label">Trudmesaĝujo</string>
|
||||
<string name="account_setup_special_folders_drafts_folder_label">Malnetujo</string>
|
||||
<string name="account_setup_special_folders_folder_automatic">Aŭtomate (%s)</string>
|
||||
<string name="account_setup_options_account_name_label">Nomo de konto</string>
|
||||
<string name="account_setup_options_display_name_label">Via nomo</string>
|
||||
<string name="account_setup_create_account_creating">Kreante konton…</string>
|
||||
<string name="account_setup_options_section_sync_options">Opcioj pri sinkronigado</string>
|
||||
<plurals name="account_setup_options_email_check_frequency_minutes">
|
||||
<item quantity="one">Ĉiuminute</item>
|
||||
<item quantity="other">Po unu fojo en %d minutoj</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_check_frequency_hours">
|
||||
<item quantity="one">Ĉiuhore</item>
|
||||
<item quantity="other">Po unu fojo en %d horoj</item>
|
||||
</plurals>
|
||||
<plurals name="account_setup_options_email_display_count_messages">
|
||||
<item quantity="one">1 mesaĝo</item>
|
||||
<item quantity="other">%d mesaĝoj</item>
|
||||
</plurals>
|
||||
<string name="account_setup_options_show_notifications_label">Montri sciigojn</string>
|
||||
<string name="account_setup_special_folders_archive_folder_label">Arĥivujo</string>
|
||||
<string name="account_setup_special_folders_trash_folder_label">Rubujo</string>
|
||||
<string name="account_setup_options_account_check_frequency_label">Ofto de kontrolado</string>
|
||||
<string name="account_setup_options_email_check_frequency_never">Neniam</string>
|
||||
<string name="account_setup_special_folders_folder_none">Nenio</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_supported">Tiu retpoŝtadreso estas ne subtenata.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_invalid">Tio ne estas rekonata kiel valida retpoŝtadreso.</string>
|
||||
<string name="account_setup_auto_discovery_validation_error_email_address_not_allowed">Tiu retpoŝtadreso estas ne permesata.</string>
|
||||
<string name="account_setup_options_email_display_count_label">Nombro da vidigendaj mesaĝoj</string>
|
||||
<string name="account_setup_options_section_display_options">Vidigan opcioj</string>
|
||||
<string name="account_setup_create_account_error">Eraro okazis dum provo krei la konton</string>
|
||||
<string name="account_setup_options_account_name_error_blank">La nomo konton ne povas esti malplena.</string>
|
||||
<string name="account_setup_options_display_name_error_required">Via nomo necesas.</string>
|
||||
<string name="account_setup_options_email_signature_label">Retletera subskribo</string>
|
||||
<string name="account_setup_options_email_signature_error_blank">La retletera subskribo ne povas esti malplena.</string>
|
||||
<string name="account_setup_create_account_created">Konto sukcese kreita</string>
|
||||
<string name="account_setup_special_folders_form_description">Bonvolu specifi la specialajn dosierujojn por via konto.</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