Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:56:56 +01:00
parent 75dc487a7a
commit 39c29d175b
6317 changed files with 388324 additions and 2 deletions

View 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)
}

View file

@ -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",
)
}
}

View file

@ -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,
)
}
}

View file

@ -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",
)

View file

@ -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 = {},
)
}
}

View file

@ -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 = {},
)
}
}

View file

@ -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,
)
}
}

View file

@ -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 = {},
)
}
}

View file

@ -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",
)
}
}

View file

@ -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(),
)
}
}

View file

@ -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,
)
}
}

View file

@ -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"
}

View file

@ -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",
)
}
}

View file

@ -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,
)
}
}

View file

@ -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",
)
}
}

View file

@ -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,
)
}
}

View file

@ -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",
)
}
}

View file

@ -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 = {},
)
}
}

View file

@ -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,
)
}
}

View file

@ -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)
}
}

View file

@ -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?
}
}

View file

@ -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(),
)
}
}

View file

@ -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(),
)
}

View file

@ -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
}
}
}
}

View file

@ -0,0 +1,4 @@
package app.k9mail.feature.account.setup.domain.entity
@JvmInline
value class AccountUuid(val value: String)

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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")
}
}

View file

@ -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,
)
}
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -0,0 +1,5 @@
package app.k9mail.feature.account.setup.navigation
import app.k9mail.core.ui.compose.navigation.Navigation
interface AccountSetupNavigation : Navigation<AccountSetupRoute>

View file

@ -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"
}
}

View file

@ -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,
)
}
}
}
}

View file

@ -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,
)
}
}
}

View file

@ -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?,
)
}

View file

@ -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,
)
}

View file

@ -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),
)
}

View file

@ -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)
}
}

View file

@ -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,
)
}
}

View file

@ -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,
)
}
}

View file

@ -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(),
)
}

View file

@ -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,
)
}
}

View file

@ -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,
),
}

View file

@ -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
}
}

View file

@ -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,
)
}
}
}
}
}
}

View file

@ -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,
)
}
}

View file

@ -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),
)
}
}

View file

@ -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
}
}

View file

@ -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,
)
}
}

View file

@ -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))
}
}

View file

@ -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))
}
}
}
}

View file

@ -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
}
}

View file

@ -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,
)
}
}

View file

@ -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() },
)
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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))
}
}
}
}

View file

@ -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
}
}

View file

@ -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,
)
}
}

View file

@ -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,
)
}

View file

@ -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),
)
}
}

View file

@ -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)
}

View file

@ -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),
)
}

View file

@ -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
}
}

View file

@ -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()),
)
}
}
}

View file

@ -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(),
)
}

View file

@ -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,
)
}
}

View file

@ -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,
)
}
}

View file

@ -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
}
}
}

View file

@ -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()
}
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View 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>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View 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>

View 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>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View 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>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View 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>

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="account_setup_auto_discovery_validation_error_email_address_required">Lindirizzu elettronicu hè richiestu.</string>
<string name="account_setup_auto_discovery_validation_error_email_address_not_allowed">Stindirizzu elettronicu ùn hè micca permessu.</string>
<string name="account_setup_auto_discovery_validation_error_email_address_invalid">Stindirizzu 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 dappruvà 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 larchivii</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 daffissera</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">Stindirizzu elettronicu ùn hè micca accettatu.</string>
<string name="account_setup_auto_discovery_validation_error_password_required">A parolla dintesa 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 labbia alterata. Puderiate verificà torna a cunfigurazione pruvista per assicurassi chella 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">Lozzione « 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>

View 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>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View 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>

View 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>

View 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>

View 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>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View 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