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,33 @@
plugins {
id(ThunderbirdPlugins.Library.androidCompose)
}
android {
namespace = "net.thunderbird.feature.navigation.drawer.dropdown"
resourcePrefix = "navigation_drawer_dropdown_"
}
dependencies {
api(projects.feature.navigation.drawer.api)
implementation(projects.core.android.account)
implementation(projects.core.ui.theme.api)
implementation(projects.core.ui.compose.designsystem)
implementation(projects.feature.account.avatar.impl)
implementation(projects.feature.mail.account.api)
implementation(projects.feature.mail.folder.api)
implementation(projects.feature.search.implLegacy)
implementation(projects.legacy.mailstore)
implementation(projects.legacy.message)
implementation(projects.legacy.ui.folder)
implementation(projects.core.featureflag)
testImplementation(projects.core.ui.compose.testing)
testImplementation(projects.core.logging.testing)
// Fakes
debugImplementation(projects.feature.account.fake)
testImplementation(projects.feature.account.fake)
}

View file

@ -0,0 +1,287 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import kotlinx.collections.immutable.persistentListOf
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract.DrawerConfig
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.DISPLAY_FOLDER
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.MAIL_DISPLAY_ACCOUNT
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.UNIFIED_FOLDER
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.createAccountList
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.createDisplayFolderList
@Composable
@Preview(showBackground = true)
internal fun DrawerContentPreview() {
PreviewWithTheme {
DrawerContent(
state = DrawerContract.State(
accounts = persistentListOf(),
selectedAccountId = null,
folders = persistentListOf(),
),
onEvent = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun DrawerContentWithAccountPreview() {
PreviewWithTheme {
DrawerContent(
state = DrawerContract.State(
accounts = persistentListOf(MAIL_DISPLAY_ACCOUNT),
selectedAccountId = MAIL_DISPLAY_ACCOUNT.id,
folders = persistentListOf(),
),
onEvent = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun DrawerContentWithFoldersPreview() {
PreviewWithTheme {
DrawerContent(
state = DrawerContract.State(
accounts = persistentListOf(
MAIL_DISPLAY_ACCOUNT,
),
selectedAccountId = null,
folders = persistentListOf(
UNIFIED_FOLDER,
DISPLAY_FOLDER,
),
),
onEvent = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun DrawerContentWithSelectedFolderPreview() {
PreviewWithTheme {
DrawerContent(
state = DrawerContract.State(
accounts = persistentListOf(
MAIL_DISPLAY_ACCOUNT,
),
selectedAccountId = MAIL_DISPLAY_ACCOUNT.id,
folders = persistentListOf(
UNIFIED_FOLDER,
DISPLAY_FOLDER,
),
selectedFolderId = DISPLAY_FOLDER.id,
),
onEvent = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun DrawerContentWithSelectedUnifiedFolderPreview() {
PreviewWithTheme {
DrawerContent(
state = DrawerContract.State(
accounts = persistentListOf(
MAIL_DISPLAY_ACCOUNT,
),
selectedAccountId = MAIL_DISPLAY_ACCOUNT.id,
folders = persistentListOf(
UNIFIED_FOLDER,
DISPLAY_FOLDER,
),
selectedFolderId = UNIFIED_FOLDER.id,
),
onEvent = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun DrawerContentSingleAccountPreview() {
val displayFolders = createDisplayFolderList(hasUnifiedFolder = false)
PreviewWithTheme {
DrawerContent(
state = DrawerContract.State(
accounts = persistentListOf(
MAIL_DISPLAY_ACCOUNT,
),
selectedAccountId = MAIL_DISPLAY_ACCOUNT.id,
folders = displayFolders,
selectedFolderId = displayFolders[0].id,
config = DrawerConfig(
showUnifiedFolders = false,
showStarredCount = false,
showAccountSelector = false,
),
),
onEvent = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun DrawerContentSingleAccountWithAccountSelectionPreview() {
val displayFolders = createDisplayFolderList(hasUnifiedFolder = false)
PreviewWithTheme {
DrawerContent(
state = DrawerContract.State(
accounts = persistentListOf(
MAIL_DISPLAY_ACCOUNT,
),
selectedAccountId = MAIL_DISPLAY_ACCOUNT.id,
folders = displayFolders,
selectedFolderId = displayFolders[0].id,
config = DrawerConfig(
showUnifiedFolders = false,
showStarredCount = false,
showAccountSelector = true,
),
),
onEvent = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun DrawerContentMultipleAccountsAccountPreview() {
val accountList = createAccountList()
val displayFolders = createDisplayFolderList(hasUnifiedFolder = true)
PreviewWithTheme {
DrawerContent(
state = DrawerContract.State(
accounts = accountList,
selectedAccountId = accountList[0].id,
folders = displayFolders,
selectedFolderId = UNIFIED_FOLDER.id,
config = DrawerConfig(
showUnifiedFolders = false,
showStarredCount = false,
showAccountSelector = false,
),
),
onEvent = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun DrawerContentMultipleAccountsWithAccountSelectionPreview() {
val accountList = createAccountList()
PreviewWithTheme {
DrawerContent(
state = DrawerContract.State(
accounts = accountList,
selectedAccountId = accountList[1].id,
folders = createDisplayFolderList(hasUnifiedFolder = true),
selectedFolderId = UNIFIED_FOLDER.id,
config = DrawerConfig(
showUnifiedFolders = false,
showStarredCount = false,
showAccountSelector = true,
),
),
onEvent = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun DrawerContentMultipleAccountsWithDifferentAccountSelectionPreview() {
val accountList = createAccountList()
PreviewWithTheme {
DrawerContent(
state = DrawerContract.State(
accounts = accountList,
selectedAccountId = accountList[2].id,
folders = createDisplayFolderList(hasUnifiedFolder = true),
selectedFolderId = UNIFIED_FOLDER.id,
config = DrawerConfig(
showUnifiedFolders = false,
showStarredCount = false,
showAccountSelector = true,
),
),
onEvent = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun DrawerContentSmallScreenPreview() {
val accountList = createAccountList()
PreviewWithTheme {
Surface(
modifier = Modifier
.width(320.dp)
.height(480.dp),
) {
DrawerContent(
state = DrawerContract.State(
accounts = accountList,
selectedAccountId = accountList[2].id,
folders = createDisplayFolderList(hasUnifiedFolder = true),
selectedFolderId = UNIFIED_FOLDER.id,
config = DrawerConfig(
showUnifiedFolders = false,
showStarredCount = false,
showAccountSelector = true,
),
),
onEvent = {},
)
}
}
}
@Composable
@Preview(showBackground = true)
internal fun DrawerContentVerySmallScreenPreview() {
val accountList = createAccountList()
PreviewWithTheme {
Surface(
modifier = Modifier
.width(240.dp)
.height(320.dp),
) {
DrawerContent(
state = DrawerContract.State(
accounts = accountList,
selectedAccountId = accountList[2].id,
folders = createDisplayFolderList(hasUnifiedFolder = true),
selectedFolderId = UNIFIED_FOLDER.id,
config = DrawerConfig(
showUnifiedFolders = false,
showStarredCount = false,
showAccountSelector = true,
),
),
onEvent = {},
)
}
}
}

View file

@ -0,0 +1,237 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import net.thunderbird.account.fake.FakeAccountData.ACCOUNT_ID_RAW
import net.thunderbird.core.android.account.Identity
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.feature.mail.folder.api.Folder
import net.thunderbird.feature.mail.folder.api.FolderType
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayTreeFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.MailDisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.MailDisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolderType
internal object FakeData {
const val DISPLAY_NAME = "Account Name"
const val EMAIL_ADDRESS = "test@example.com"
val ACCOUNT = LegacyAccount(
uuid = ACCOUNT_ID_RAW,
).apply {
identities = ArrayList()
val identity = Identity(
signatureUse = false,
signature = "",
description = "",
)
identities.add(identity)
name = DISPLAY_NAME
email = EMAIL_ADDRESS
}
val UNIFIED_DISPLAY_ACCOUNT = UnifiedDisplayAccount(
unreadMessageCount = 224,
starredMessageCount = 42,
)
val MAIL_DISPLAY_ACCOUNT = MailDisplayAccount(
id = ACCOUNT_ID_RAW,
name = DISPLAY_NAME,
email = EMAIL_ADDRESS,
color = Color.Red.toArgb(),
unreadMessageCount = 0,
starredMessageCount = 0,
)
val FOLDER = Folder(
id = 1,
name = "Folder Name",
type = FolderType.REGULAR,
isLocalOnly = false,
)
val DISPLAY_FOLDER = MailDisplayFolder(
accountId = ACCOUNT_ID_RAW,
folder = FOLDER,
isInTopGroup = false,
unreadMessageCount = 14,
starredMessageCount = 5,
pathDelimiter = "/",
)
val DISPLAY_TREE_FOLDER = DisplayTreeFolder(
displayFolder = null,
displayName = null,
totalUnreadCount = 14,
totalStarredCount = 5,
children = persistentListOf(
DisplayTreeFolder(
displayFolder = DISPLAY_FOLDER,
displayName = DISPLAY_FOLDER.folder.name,
totalUnreadCount = 14,
totalStarredCount = 5,
children = persistentListOf(),
),
),
)
val EMPTY_DISPLAY_TREE_FOLDER = DisplayTreeFolder(
displayFolder = null,
displayName = null,
totalUnreadCount = 0,
totalStarredCount = 0,
children = persistentListOf(),
)
val UNIFIED_FOLDER = UnifiedDisplayFolder(
id = "unified_inbox",
unifiedType = UnifiedDisplayFolderType.INBOX,
unreadMessageCount = 123,
starredMessageCount = 567,
)
val DISPLAY_TREE_FOLDER_WITH_UNIFIED_FOLDER = DisplayTreeFolder(
displayFolder = null,
displayName = null,
totalUnreadCount = 14,
totalStarredCount = 5,
children = persistentListOf(
DisplayTreeFolder(
displayFolder = UNIFIED_FOLDER,
displayName = null,
totalUnreadCount = 7,
totalStarredCount = 2,
children = persistentListOf(),
),
DisplayTreeFolder(
displayFolder = DISPLAY_FOLDER,
displayName = DISPLAY_FOLDER.folder.name,
totalUnreadCount = 7,
totalStarredCount = 3,
children = persistentListOf(),
),
),
)
val DISPLAY_TREE_FOLDER_WITH_NESTED_FOLDERS = DisplayTreeFolder(
displayFolder = null,
displayName = null,
totalUnreadCount = 14,
totalStarredCount = 5,
children = persistentListOf(
DisplayTreeFolder(
displayFolder = DISPLAY_FOLDER,
displayName = DISPLAY_FOLDER.folder.name,
totalUnreadCount = 7,
totalStarredCount = 3,
children = persistentListOf(
DisplayTreeFolder(
displayFolder = null,
displayName = null,
totalUnreadCount = 7,
totalStarredCount = 3,
children = persistentListOf(),
),
),
),
),
)
fun createAccountList(): PersistentList<MailDisplayAccount> {
return persistentListOf(
MailDisplayAccount(
id = "account1",
name = "job@example.com",
email = "job@example.com",
color = Color.Green.toArgb(),
unreadMessageCount = 2,
starredMessageCount = 0,
),
MailDisplayAccount(
id = "account2",
name = "Jodie Doe",
email = "jodie@example.com",
color = Color.Red.toArgb(),
unreadMessageCount = 12,
starredMessageCount = 0,
),
MailDisplayAccount(
id = "account3",
name = "John Doe",
email = "john@example.com",
color = Color.Cyan.toArgb(),
unreadMessageCount = 0,
starredMessageCount = 0,
),
)
}
fun createDisplayFolderList(hasUnifiedFolder: Boolean): PersistentList<DisplayFolder> {
val folders = mutableListOf<DisplayFolder>()
if (hasUnifiedFolder) {
folders.add(UNIFIED_FOLDER)
}
folders.addAll(
listOf(
DISPLAY_FOLDER.copy(
folder = FOLDER.copy(id = 2, name = "Inbox", type = FolderType.INBOX),
unreadMessageCount = 12,
),
DISPLAY_FOLDER.copy(
folder = FOLDER.copy(id = 3, name = "Outbox", type = FolderType.OUTBOX),
unreadMessageCount = 0,
),
DISPLAY_FOLDER.copy(
folder = FOLDER.copy(id = 4, name = "Drafts", type = FolderType.DRAFTS),
unreadMessageCount = 0,
),
DISPLAY_FOLDER.copy(
folder = FOLDER.copy(id = 5, name = "Sent", type = FolderType.SENT),
unreadMessageCount = 0,
),
DISPLAY_FOLDER.copy(
folder = FOLDER.copy(id = 6, name = "Spam", type = FolderType.SPAM),
unreadMessageCount = 5,
),
DISPLAY_FOLDER.copy(
folder = FOLDER.copy(id = 7, name = "Trash", type = FolderType.TRASH),
unreadMessageCount = 0,
),
DISPLAY_FOLDER.copy(
folder = FOLDER.copy(id = 8, name = "Archive", type = FolderType.ARCHIVE),
unreadMessageCount = 0,
),
DISPLAY_FOLDER.copy(
folder = FOLDER.copy(id = 9, name = "Work", type = FolderType.REGULAR),
unreadMessageCount = 3,
),
DISPLAY_FOLDER.copy(
folder = FOLDER.copy(id = 10, name = "Personal", type = FolderType.REGULAR),
unreadMessageCount = 4,
),
DISPLAY_FOLDER.copy(
folder = FOLDER.copy(id = 11, name = "Important", type = FolderType.REGULAR),
unreadMessageCount = 0,
),
DISPLAY_FOLDER.copy(
folder = FOLDER.copy(id = 12, name = "Later", type = FolderType.REGULAR),
unreadMessageCount = 0,
),
),
)
return folders.toPersistentList()
}
}

View file

@ -0,0 +1,60 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.account
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.MAIL_DISPLAY_ACCOUNT
@Composable
@Preview(showBackground = true)
internal fun AccountAvatarPreview() {
PreviewWithThemes {
AccountAvatar(
account = MAIL_DISPLAY_ACCOUNT,
onClick = {},
selected = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountAvatarWithUnreadCountPreview() {
PreviewWithThemes {
AccountAvatar(
account = MAIL_DISPLAY_ACCOUNT.copy(
unreadMessageCount = 12,
),
onClick = {},
selected = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountAvatarWithUnreadCountMaxedPreview() {
PreviewWithThemes {
AccountAvatar(
account = MAIL_DISPLAY_ACCOUNT.copy(
unreadMessageCount = 100,
),
onClick = {},
selected = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountAvatarSelectedPreview() {
PreviewWithThemes {
AccountAvatar(
account = MAIL_DISPLAY_ACCOUNT.copy(
color = 0xFFFF0000.toInt(),
),
onClick = {},
selected = true,
)
}
}

View file

@ -0,0 +1,53 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.account
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 AccountListItemBadgePreview() {
PreviewWithThemes {
AccountListItemBadge(
unreadCount = 5,
starredCount = 3,
showStarredCount = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountListItemBadgeWithMaxUnreadPreview() {
PreviewWithThemes {
AccountListItemBadge(
unreadCount = 999,
starredCount = 0,
showStarredCount = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountListItemBadgeWithStarsPreview() {
PreviewWithThemes {
AccountListItemBadge(
unreadCount = 5,
starredCount = 3,
showStarredCount = true,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountListItemBadgeWithStarsAndMaxCountPreview() {
PreviewWithThemes {
AccountListItemBadge(
unreadCount = 5,
starredCount = 999,
showStarredCount = true,
)
}
}

View file

@ -0,0 +1,32 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.account
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.MAIL_DISPLAY_ACCOUNT
@Composable
@Preview(showBackground = true)
internal fun AccountListItemPreview() {
PreviewWithThemes {
AccountListItem(
account = MAIL_DISPLAY_ACCOUNT,
onClick = { },
selected = false,
showStarredCount = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountListItemSelectedPreview() {
PreviewWithThemes {
AccountListItem(
account = MAIL_DISPLAY_ACCOUNT,
onClick = { },
selected = true,
showStarredCount = false,
)
}
}

View file

@ -0,0 +1,37 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.account
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
import kotlinx.collections.immutable.persistentListOf
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.MAIL_DISPLAY_ACCOUNT
@Composable
@Preview(showBackground = true)
internal fun AccountListPreview() {
PreviewWithTheme {
AccountList(
accounts = persistentListOf(
MAIL_DISPLAY_ACCOUNT,
),
selectedAccount = null,
onAccountClick = { },
showStarredCount = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountListWithSelectedPreview() {
PreviewWithTheme {
AccountList(
accounts = persistentListOf(
MAIL_DISPLAY_ACCOUNT,
),
selectedAccount = MAIL_DISPLAY_ACCOUNT,
onAccountClick = { },
showStarredCount = false,
)
}
}

View file

@ -0,0 +1,30 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.account
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.MAIL_DISPLAY_ACCOUNT
@Composable
@Preview(showBackground = true)
internal fun AccountViewPreview() {
PreviewWithThemes {
AccountView(
account = MAIL_DISPLAY_ACCOUNT,
onClick = {},
showAccountSelection = true,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountViewWithoutAccountPreview() {
PreviewWithThemes {
AccountView(
account = MAIL_DISPLAY_ACCOUNT,
onClick = {},
showAccountSelection = false,
)
}
}

View file

@ -0,0 +1,101 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.folder
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 FolderListItemBadgePreview() {
PreviewWithThemes {
FolderListItemBadge(
unreadCount = 99,
starredCount = 0,
showStarredCount = true,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemBadgeWithStarredCountPreview() {
PreviewWithThemes {
FolderListItemBadge(
unreadCount = 99,
starredCount = 1,
showStarredCount = true,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemBadgeWithZeroUnreadCountPreview() {
PreviewWithThemes {
FolderListItemBadge(
unreadCount = 0,
starredCount = 1,
showStarredCount = true,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemBadgeWithZeroStarredCountPreview() {
PreviewWithThemes {
FolderListItemBadge(
unreadCount = 99,
starredCount = 0,
showStarredCount = true,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemBadgeWithZeroCountsPreview() {
PreviewWithThemes {
FolderListItemBadge(
unreadCount = 0,
starredCount = 0,
showStarredCount = true,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemBadgeWithoutStarredCountPreview() {
PreviewWithThemes {
FolderListItemBadge(
unreadCount = 99,
starredCount = 1,
showStarredCount = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemBadgeWith100CountsPreview() {
PreviewWithThemes {
FolderListItemBadge(
unreadCount = 100,
starredCount = 100,
showStarredCount = true,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemBadgeWith1000CountsPreview() {
PreviewWithThemes {
FolderListItemBadge(
unreadCount = 1000,
starredCount = 1000,
showStarredCount = true,
)
}
}

View file

@ -0,0 +1,129 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.folder
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import app.k9mail.legacy.ui.folder.FolderNameFormatter
import net.thunderbird.feature.mail.folder.api.FolderType
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.DISPLAY_FOLDER
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.DISPLAY_TREE_FOLDER_WITH_UNIFIED_FOLDER
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.UNIFIED_FOLDER
@Composable
@Preview(showBackground = true)
internal fun FolderListItemPreview() {
PreviewWithThemes {
FolderListItem(
displayFolder = DISPLAY_FOLDER,
selectedFolderId = "unknown",
showStarredCount = false,
onClick = {},
folderNameFormatter = FolderNameFormatter(LocalContext.current.resources),
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemSelectedPreview() {
PreviewWithThemes {
FolderListItem(
displayFolder = DISPLAY_FOLDER,
selectedFolderId = DISPLAY_FOLDER.id,
showStarredCount = false,
onClick = {},
folderNameFormatter = FolderNameFormatter(LocalContext.current.resources),
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemWithStarredPreview() {
PreviewWithThemes {
FolderListItem(
displayFolder = DISPLAY_FOLDER,
selectedFolderId = "unknown",
showStarredCount = true,
onClick = {},
folderNameFormatter = FolderNameFormatter(LocalContext.current.resources),
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemWithStarredSelectedPreview() {
PreviewWithThemes {
FolderListItem(
displayFolder = DISPLAY_FOLDER,
selectedFolderId = DISPLAY_FOLDER.id,
showStarredCount = true,
onClick = {},
folderNameFormatter = FolderNameFormatter(LocalContext.current.resources),
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemWithInboxFolderPreview() {
PreviewWithThemes {
FolderListItem(
displayFolder = DISPLAY_FOLDER.copy(
folder = DISPLAY_FOLDER.folder.copy(
type = FolderType.INBOX,
),
),
selectedFolderId = "unknown",
showStarredCount = true,
onClick = {},
folderNameFormatter = FolderNameFormatter(LocalContext.current.resources),
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemWithUnifiedFolderPreview() {
PreviewWithThemes {
FolderListItem(
displayFolder = UNIFIED_FOLDER,
selectedFolderId = "unknown",
showStarredCount = false,
onClick = {},
folderNameFormatter = FolderNameFormatter(LocalContext.current.resources),
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemWithUnifiedFolderSelectedPreview() {
PreviewWithThemes {
FolderListItem(
displayFolder = UNIFIED_FOLDER,
treeFolder = DISPLAY_TREE_FOLDER_WITH_UNIFIED_FOLDER,
selectedFolderId = UNIFIED_FOLDER.id,
showStarredCount = false,
onClick = {},
folderNameFormatter = FolderNameFormatter(LocalContext.current.resources),
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListItemStarredCountPreview() {
PreviewWithThemes {
FolderListItem(
displayFolder = UNIFIED_FOLDER,
treeFolder = DISPLAY_TREE_FOLDER_WITH_UNIFIED_FOLDER,
selectedFolderId = null,
showStarredCount = true,
onClick = {},
folderNameFormatter = FolderNameFormatter(LocalContext.current.resources),
)
}
}

View file

@ -0,0 +1,62 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.folder
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.DISPLAY_FOLDER
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.DISPLAY_TREE_FOLDER
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.DISPLAY_TREE_FOLDER_WITH_NESTED_FOLDERS
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.DISPLAY_TREE_FOLDER_WITH_UNIFIED_FOLDER
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.EMPTY_DISPLAY_TREE_FOLDER
@Composable
@Preview(showBackground = true)
internal fun FolderListPreview() {
PreviewWithTheme {
FolderList(
rootFolder = EMPTY_DISPLAY_TREE_FOLDER,
selectedFolder = null,
onFolderClick = {},
showStarredCount = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListPreviewSelected() {
PreviewWithTheme {
FolderList(
rootFolder = DISPLAY_TREE_FOLDER,
selectedFolder = DISPLAY_FOLDER,
onFolderClick = {},
showStarredCount = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListWithUnifiedFolderPreview() {
PreviewWithTheme {
FolderList(
rootFolder = DISPLAY_TREE_FOLDER_WITH_UNIFIED_FOLDER,
selectedFolder = DISPLAY_FOLDER,
onFolderClick = {},
showStarredCount = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun FolderListWithUnifiedFolderPreviewSelected() {
PreviewWithTheme {
FolderList(
rootFolder = DISPLAY_TREE_FOLDER_WITH_NESTED_FOLDERS,
selectedFolder = null,
onFolderClick = {},
showStarredCount = false,
)
}
}

View file

@ -0,0 +1,18 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.setting
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
@Composable
@Preview(showBackground = true)
internal fun SettingListItemPreview() {
PreviewWithThemes {
SettingListItem(
label = "Settings",
onClick = {},
icon = Icons.Outlined.Settings,
)
}
}

View file

@ -0,0 +1,29 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.setting
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 SettingListPreview() {
PreviewWithTheme {
FolderSettingList(
onManageFoldersClick = {},
onSettingsClick = {},
isUnifiedAccount = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun SettingListWithUnifiedAccountPreview() {
PreviewWithTheme {
FolderSettingList(
onManageFoldersClick = {},
onSettingsClick = {},
isUnifiedAccount = true,
)
}
}

View file

@ -0,0 +1,42 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui.account
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import app.k9mail.core.ui.compose.theme2.MainTheme
@Composable
@Preview(showBackground = true)
internal fun SideRailAccountIndicatorPreview() {
PreviewWithThemes {
SideRailAccountIndicator(
accountColor = Color.Unspecified,
modifier = Modifier.height(MainTheme.spacings.double),
)
}
}
@Composable
@Preview(showBackground = true)
internal fun SideRailAccountIndicatorPreviewWithYellowAccountColor() {
PreviewWithThemes {
SideRailAccountIndicator(
accountColor = Color.Yellow,
modifier = Modifier.height(MainTheme.spacings.double),
)
}
}
@Composable
@Preview(showBackground = true)
internal fun SideRailAccountIndicatorPreviewWithGrayAccountColor() {
PreviewWithThemes {
SideRailAccountIndicator(
accountColor = Color.Gray,
modifier = Modifier.height(MainTheme.spacings.double),
)
}
}

View file

@ -0,0 +1,30 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui.account
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.MAIL_DISPLAY_ACCOUNT
@Composable
@Preview(showBackground = true)
internal fun SideRailAccountListItemPreview() {
PreviewWithThemes {
SideRailAccountListItem(
account = MAIL_DISPLAY_ACCOUNT,
onClick = { },
selected = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun SideRailAccountListItemSelectedPreview() {
PreviewWithThemes {
SideRailAccountListItem(
account = MAIL_DISPLAY_ACCOUNT,
onClick = { },
selected = true,
)
}
}

View file

@ -0,0 +1,39 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui.account
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
import kotlinx.collections.immutable.persistentListOf
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.MAIL_DISPLAY_ACCOUNT
@Composable
@Preview(showBackground = true)
internal fun SideRailAccountListPreview() {
PreviewWithTheme {
SideRailAccountList(
accounts = persistentListOf(
MAIL_DISPLAY_ACCOUNT,
),
selectedAccount = null,
onAccountClick = { },
onSettingsClick = { },
onSyncAllAccountsClick = { },
)
}
}
@Composable
@Preview(showBackground = true)
internal fun SideRailAccountListWithSelectedPreview() {
PreviewWithTheme {
SideRailAccountList(
accounts = persistentListOf(
MAIL_DISPLAY_ACCOUNT,
),
selectedAccount = MAIL_DISPLAY_ACCOUNT,
onAccountClick = { },
onSettingsClick = { },
onSyncAllAccountsClick = { },
)
}
}

View file

@ -0,0 +1,54 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui.account
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import net.thunderbird.feature.navigation.drawer.dropdown.ui.FakeData.MAIL_DISPLAY_ACCOUNT
@Composable
@Preview(showBackground = true)
internal fun SideRailAccountViewPreview() {
PreviewWithThemes {
SideRailAccountView(
account = MAIL_DISPLAY_ACCOUNT,
onClick = {},
showAvatar = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun SideRailAccountViewWithColorPreview() {
PreviewWithThemes {
SideRailAccountView(
account = MAIL_DISPLAY_ACCOUNT,
onClick = {},
showAvatar = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun SideRailAccountViewWithLongDisplayName() {
PreviewWithThemes {
SideRailAccountView(
account = MAIL_DISPLAY_ACCOUNT,
onClick = {},
showAvatar = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun SideRailAccountViewWithLongEmailPreview() {
PreviewWithThemes {
SideRailAccountView(
account = MAIL_DISPLAY_ACCOUNT,
onClick = {},
showAvatar = false,
)
}
}

View file

@ -0,0 +1,18 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui.setting
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
@Composable
@Preview(showBackground = true)
internal fun SideRailSettingItemPreview() {
PreviewWithThemes {
SideRailSettingItem(
icon = Icons.Outlined.Settings,
label = "Setting",
onClick = {},
)
}
}

View file

@ -0,0 +1,29 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui.setting
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 SideRailSettingListPreview() {
PreviewWithTheme {
SideRailSettingList(
onAccountSelectorClick = {},
onManageFoldersClick = {},
showAccountSelector = false,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun SideRailSettingListShowAccountSelectorPreview() {
PreviewWithTheme {
SideRailSettingList(
onAccountSelectorClick = {},
onManageFoldersClick = {},
showAccountSelector = true,
)
}
}

View file

@ -0,0 +1,118 @@
package net.thunderbird.feature.navigation.drawer.dropdown
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import net.thunderbird.core.featureflag.FeatureFlagProvider
import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawer
import net.thunderbird.feature.navigation.drawer.api.R
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolderType
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.createMailDisplayAccountFolderId
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerView
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
internal data class FolderDrawerState(
val selectedAccountUuid: String? = null,
val selectedFolderId: String? = null,
)
@Suppress("LongParameterList")
class DropDownDrawer(
override val parent: AppCompatActivity,
private val openAccount: (accountId: String) -> Unit,
private val openFolder: (accountId: String, folderId: Long) -> Unit,
private val openUnifiedFolder: () -> Unit,
private val openManageFolders: () -> Unit,
private val openSettings: () -> Unit,
private val openAddAccount: () -> Unit,
createDrawerListener: () -> DrawerLayout.DrawerListener,
) : NavigationDrawer, KoinComponent {
private val themeProvider: FeatureThemeProvider by inject()
private val featureFlagProvider: FeatureFlagProvider by inject()
private val drawer: DrawerLayout = parent.findViewById(R.id.navigation_drawer_layout)
private val drawerContent: ComposeView = parent.findViewById(R.id.navigation_drawer_content)
private val drawerState = MutableStateFlow(FolderDrawerState())
init {
drawer.addDrawerListener(createDrawerListener())
drawerContent.setContent {
themeProvider.WithTheme {
val state = drawerState.collectAsStateWithLifecycle()
DrawerView(
drawerState = state.value,
openAccount = openAccount,
openFolder = openFolder,
openUnifiedFolder = openUnifiedFolder,
openManageFolders = openManageFolders,
openSettings = openSettings,
openAddAccount = openAddAccount,
featureFlagProvider = featureFlagProvider,
closeDrawer = { close() },
)
}
}
}
override val isOpen: Boolean
get() = drawer.isOpen
override fun selectAccount(accountUuid: String) {
drawerState.update {
it.copy(selectedAccountUuid = accountUuid)
}
}
override fun selectFolder(accountUuid: String, folderId: Long) {
drawerState.update {
it.copy(
selectedAccountUuid = accountUuid,
selectedFolderId = createMailDisplayAccountFolderId(accountUuid, folderId),
)
}
}
override fun selectUnifiedInbox() {
drawerState.update {
it.copy(
selectedAccountUuid = UnifiedDisplayAccount.UNIFIED_ACCOUNT_ID,
selectedFolderId = UnifiedDisplayFolderType.INBOX.id,
)
}
}
override fun deselect() {
drawerState.update {
it.copy(
selectedFolderId = null,
)
}
}
override fun open() {
drawer.openDrawer(GravityCompat.START)
}
override fun close() {
drawer.closeDrawer(GravityCompat.START)
}
override fun lock() {
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
}
override fun unlock() {
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
}
}

View file

@ -0,0 +1,82 @@
package net.thunderbird.feature.navigation.drawer.dropdown
import net.thunderbird.feature.navigation.drawer.dropdown.data.UnifiedFolderRepository
import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract
import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract.UseCase
import net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase.GetDisplayAccounts
import net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase.GetDisplayFoldersForAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase.GetDisplayTreeFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase.GetDrawerConfig
import net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase.SaveDrawerConfig
import net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase.SyncAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase.SyncAllAccounts
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerViewModel
import org.koin.core.module.Module
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
val navigationDropDownDrawerModule: Module = module {
single<DomainContract.UnifiedFolderRepository> {
UnifiedFolderRepository(
messageCountsProvider = get(),
)
}
single<UseCase.GetDrawerConfig> {
GetDrawerConfig(
configLoader = get(),
)
}
single<UseCase.SaveDrawerConfig> {
SaveDrawerConfig(
drawerConfigWriter = get(),
)
}
single<UseCase.GetDisplayAccounts> {
GetDisplayAccounts(
accountManager = get(),
messageCountsProvider = get(),
messageListRepository = get(),
)
}
single<UseCase.GetDisplayFoldersForAccount> {
GetDisplayFoldersForAccount(
displayFolderRepository = get(),
unifiedFolderRepository = get(),
)
}
single<UseCase.GetDisplayTreeFolder> {
GetDisplayTreeFolder(
logger = get(),
)
}
single<UseCase.SyncAccount> {
SyncAccount(
accountManager = get(),
messagingController = get(),
)
}
single<UseCase.SyncAllAccounts> {
SyncAllAccounts(
messagingController = get(),
)
}
viewModel {
DrawerViewModel(
getDrawerConfig = get(),
saveDrawerConfig = get(),
getDisplayAccounts = get(),
getDisplayFoldersForAccount = get(),
getDisplayTreeFolder = get(),
syncAccount = get(),
syncAllAccounts = get(),
)
}
}

View file

@ -0,0 +1,44 @@
package net.thunderbird.feature.navigation.drawer.dropdown.data
import app.k9mail.legacy.message.controller.MessageCountsProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolderType
import net.thunderbird.feature.search.legacy.LocalMessageSearch
import net.thunderbird.feature.search.legacy.api.MessageSearchField
import net.thunderbird.feature.search.legacy.api.SearchAttribute
internal class UnifiedFolderRepository(
private val messageCountsProvider: MessageCountsProvider,
) : DomainContract.UnifiedFolderRepository {
override fun getUnifiedDisplayFolderFlow(unifiedFolderType: UnifiedDisplayFolderType): Flow<UnifiedDisplayFolder> {
return messageCountsProvider.getMessageCountsFlow(createUnifiedFolderSearch(unifiedFolderType)).map {
UnifiedDisplayFolder(
id = UNIFIED_INBOX_ID,
unifiedType = UnifiedDisplayFolderType.INBOX,
unreadMessageCount = it.unread,
starredMessageCount = it.starred,
)
}
}
private fun createUnifiedFolderSearch(unifiedFolderType: UnifiedDisplayFolderType): LocalMessageSearch {
return when (unifiedFolderType) {
UnifiedDisplayFolderType.INBOX -> return createUnifiedInboxSearch()
}
}
private fun createUnifiedInboxSearch(): LocalMessageSearch {
return LocalMessageSearch().apply {
id = UNIFIED_INBOX_ID
and(MessageSearchField.INTEGRATE, "1", SearchAttribute.EQUALS)
}
}
companion object {
const val UNIFIED_INBOX_ID = "unified_inbox"
}
}

View file

@ -0,0 +1,52 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain
import kotlinx.coroutines.flow.Flow
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract.DrawerConfig
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayTreeFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolderType
internal interface DomainContract {
interface UseCase {
fun interface GetDrawerConfig {
operator fun invoke(): Flow<DrawerConfig>
}
fun interface SaveDrawerConfig {
operator fun invoke(drawerConfig: DrawerConfig): Flow<Unit>
}
fun interface GetDisplayAccounts {
operator fun invoke(showUnifiedAccount: Boolean): Flow<List<DisplayAccount>>
}
fun interface GetDisplayFoldersForAccount {
operator fun invoke(accountId: String): Flow<List<DisplayFolder>>
}
fun interface GetDisplayTreeFolder {
operator fun invoke(folders: List<DisplayFolder>, maxDepth: Int): DisplayTreeFolder
}
/**
* Synchronize the given account uuid.
*/
fun interface SyncAccount {
operator fun invoke(accountUuid: String): Flow<Result<Unit>>
}
/**
* Synchronize all accounts.
*/
fun interface SyncAllAccounts {
operator fun invoke(): Flow<Result<Unit>>
}
}
interface UnifiedFolderRepository {
fun getUnifiedDisplayFolderFlow(unifiedFolderType: UnifiedDisplayFolderType): Flow<UnifiedDisplayFolder>
}
}

View file

@ -0,0 +1,7 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.entity
sealed interface DisplayAccount {
val id: String
val unreadMessageCount: Int
val starredMessageCount: Int
}

View file

@ -0,0 +1,10 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.entity
import net.thunderbird.feature.mail.folder.api.FolderPathDelimiter
internal interface DisplayFolder {
val id: String
val unreadMessageCount: Int
val starredMessageCount: Int
val pathDelimiter: FolderPathDelimiter
}

View file

@ -0,0 +1,11 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.entity
import kotlinx.collections.immutable.ImmutableList
internal data class DisplayTreeFolder(
val displayFolder: DisplayFolder?,
val displayName: String?,
val totalUnreadCount: Int,
val totalStarredCount: Int,
val children: ImmutableList<DisplayTreeFolder>,
)

View file

@ -0,0 +1,10 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.entity
internal data class MailDisplayAccount(
override val id: String,
val name: String,
val email: String,
val color: Int,
override val unreadMessageCount: Int,
override val starredMessageCount: Int,
) : DisplayAccount

View file

@ -0,0 +1,19 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.entity
import net.thunderbird.feature.mail.folder.api.Folder
import net.thunderbird.feature.mail.folder.api.FolderPathDelimiter
internal data class MailDisplayFolder(
val accountId: String?,
val folder: Folder,
val isInTopGroup: Boolean,
override val unreadMessageCount: Int,
override val starredMessageCount: Int,
override val pathDelimiter: FolderPathDelimiter,
) : DisplayFolder {
override val id: String = createMailDisplayAccountFolderId(accountId.orEmpty(), folder.id)
}
fun createMailDisplayAccountFolderId(accountId: String, folderId: Long): String {
return "${accountId}_$folderId"
}

View file

@ -0,0 +1,12 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.entity
data class UnifiedDisplayAccount(
override val unreadMessageCount: Int,
override val starredMessageCount: Int,
) : DisplayAccount {
override val id: String = UNIFIED_ACCOUNT_ID
companion object {
const val UNIFIED_ACCOUNT_ID = "unified_account"
}
}

View file

@ -0,0 +1,12 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.entity
import net.thunderbird.feature.mail.folder.api.FolderPathDelimiter
internal data class UnifiedDisplayFolder(
override val id: String,
val unifiedType: UnifiedDisplayFolderType,
override val unreadMessageCount: Int,
override val starredMessageCount: Int,
) : DisplayFolder {
override val pathDelimiter: FolderPathDelimiter = "/"
}

View file

@ -0,0 +1,12 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.entity
/**
* Represents a unified folder in the drawer.
*
* The id is unique for each unified folder type.
*/
internal enum class UnifiedDisplayFolderType(
val id: String,
) {
INBOX("unified_inbox"),
}

View file

@ -0,0 +1,85 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase
import app.k9mail.legacy.mailstore.MessageListChangedListener
import app.k9mail.legacy.mailstore.MessageListRepository
import app.k9mail.legacy.message.controller.MessageCounts
import app.k9mail.legacy.message.controller.MessageCountsProvider
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import net.thunderbird.core.android.account.AccountManager
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract.UseCase
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.MailDisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayAccount
internal class GetDisplayAccounts(
private val accountManager: AccountManager,
private val messageCountsProvider: MessageCountsProvider,
private val messageListRepository: MessageListRepository,
private val coroutineContext: CoroutineContext = Dispatchers.IO,
) : UseCase.GetDisplayAccounts {
@OptIn(ExperimentalCoroutinesApi::class)
override fun invoke(showUnifiedAccount: Boolean): Flow<List<DisplayAccount>> {
return accountManager.getAccountsFlow()
.flatMapLatest { accounts ->
val messageCountsFlows: List<Flow<MessageCounts>> = accounts.map { account ->
getMessageCountsFlow(account)
}
combine(messageCountsFlows) { messageCountsList ->
val displayAccounts = messageCountsList.mapIndexed { index, messageCounts ->
MailDisplayAccount(
id = accounts[index].uuid,
name = accounts[index].displayName,
email = accounts[index].email,
color = accounts[index].chipColor,
unreadMessageCount = messageCounts.unread,
starredMessageCount = messageCounts.starred,
)
}
if (showUnifiedAccount) {
withUnifiedAccount(displayAccounts)
} else {
displayAccounts
}
}
}
}
private fun withUnifiedAccount(accounts: List<DisplayAccount>): List<DisplayAccount> {
val unified = UnifiedDisplayAccount(
unreadMessageCount = accounts.sumOf { it.unreadMessageCount },
starredMessageCount = accounts.sumOf { it.starredMessageCount },
)
return listOf(unified) + accounts
}
private fun getMessageCountsFlow(account: LegacyAccount): Flow<MessageCounts> {
return callbackFlow {
send(messageCountsProvider.getMessageCounts(account))
val listener = MessageListChangedListener {
launch {
send(messageCountsProvider.getMessageCounts(account))
}
}
messageListRepository.addListener(account.uuid, listener)
awaitClose {
messageListRepository.removeListener(listener)
}
}.flowOn(coroutineContext)
}
}

View file

@ -0,0 +1,38 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase
import app.k9mail.legacy.ui.folder.DisplayFolderRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract.UnifiedFolderRepository
import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract.UseCase
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.MailDisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolderType
internal class GetDisplayFoldersForAccount(
private val displayFolderRepository: DisplayFolderRepository,
private val unifiedFolderRepository: UnifiedFolderRepository,
) : UseCase.GetDisplayFoldersForAccount {
override fun invoke(accountId: String): Flow<List<DisplayFolder>> {
if (accountId == UnifiedDisplayAccount.UNIFIED_ACCOUNT_ID) {
return unifiedFolderRepository.getUnifiedDisplayFolderFlow(UnifiedDisplayFolderType.INBOX)
.map { displayUnifiedFolder ->
listOf(displayUnifiedFolder)
}
} else {
return displayFolderRepository.getDisplayFoldersFlow(accountId).map { displayFolders ->
displayFolders.map { displayFolder ->
MailDisplayFolder(
accountId = accountId,
folder = displayFolder.folder,
isInTopGroup = displayFolder.isInTopGroup,
unreadMessageCount = displayFolder.unreadMessageCount,
starredMessageCount = displayFolder.starredMessageCount,
pathDelimiter = displayFolder.pathDelimiter,
)
}
}
}
}
}

View file

@ -0,0 +1,117 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import net.thunderbird.core.logging.Logger
import net.thunderbird.feature.mail.folder.api.FOLDER_DEFAULT_PATH_DELIMITER
import net.thunderbird.feature.mail.folder.api.Folder
import net.thunderbird.feature.mail.folder.api.FolderPathDelimiter
import net.thunderbird.feature.mail.folder.api.FolderType
import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract.UseCase
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayTreeFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.MailDisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolder
internal class GetDisplayTreeFolder(
private val logger: Logger,
) : UseCase.GetDisplayTreeFolder {
private var placeholderCounter = 0L
override fun invoke(folders: List<DisplayFolder>, maxDepth: Int): DisplayTreeFolder {
val unifiedFolderTreeList = folders.filterIsInstance<UnifiedDisplayFolder>().map {
DisplayTreeFolder(
displayFolder = it,
displayName = it.unifiedType.id,
totalUnreadCount = it.unreadMessageCount,
totalStarredCount = it.starredMessageCount,
children = persistentListOf(),
)
}
val pathDelimiter = folders.firstOrNull()?.pathDelimiter ?: FOLDER_DEFAULT_PATH_DELIMITER
val accountFolders = folders.filterIsInstance<MailDisplayFolder>().map {
val path = flattenPath(it.folder.name, pathDelimiter, maxDepth)
logger.debug { "Flattened path for ${it.folder.name}$path" }
path to it
}
val accountFolderTreeList = buildAccountFolderTree(accountFolders, pathDelimiter)
return DisplayTreeFolder(
displayFolder = null,
displayName = null,
totalUnreadCount = accountFolderTreeList.sumOf { it.totalUnreadCount },
totalStarredCount = accountFolderTreeList.sumOf { it.totalStarredCount },
children = (unifiedFolderTreeList + accountFolderTreeList).toImmutableList(),
)
}
private fun flattenPath(folderName: String, folderPathDelimiter: FolderPathDelimiter, maxDepth: Int): List<String> {
val parts = folderName.split(folderPathDelimiter).map { it.takeIf { it.isNotBlank() } ?: "(Unnamed)" }
return if (parts.size <= maxDepth) {
parts
} else {
parts.take(maxDepth) + listOf(parts.drop(maxDepth).joinToString(folderPathDelimiter))
}
}
private fun buildAccountFolderTree(
paths: List<Pair<List<String>, MailDisplayFolder>>,
pathDelimiter: FolderPathDelimiter,
parentPath: String = "",
): List<DisplayTreeFolder> {
return paths.groupBy { it.first.getOrNull(0) ?: "(Unnamed)" }
.map { (segment, entries) ->
val childPaths = entries.mapNotNull { (segments, folders) ->
if (segments.size > 1) {
Pair(segments.drop(1), folders)
} else {
null
}
}
val currentFolders = entries.mapNotNull { (segments, folder) ->
if (segments.size == 1) folder else null
}
val fullPath = if (parentPath.isBlank()) segment else "${parentPath}${pathDelimiter}$segment"
val currentFolder = currentFolders.firstOrNull() ?: createPlaceholderFolder(fullPath, pathDelimiter)
val children = buildAccountFolderTree(
paths = childPaths,
pathDelimiter = pathDelimiter,
parentPath = fullPath,
)
val totalUnread = children.sumOf { it.totalUnreadCount } + currentFolder.unreadMessageCount
val totalStarred = children.sumOf { it.totalStarredCount } + currentFolder.starredMessageCount
DisplayTreeFolder(
displayFolder = currentFolder,
displayName = segment,
totalUnreadCount = totalUnread,
totalStarredCount = totalStarred,
children = children.toImmutableList(),
)
}
}
private fun createPlaceholderFolder(name: String, pathDelimiter: FolderPathDelimiter): MailDisplayFolder {
placeholderCounter += 1
return MailDisplayFolder(
accountId = null,
folder = Folder(
id = placeholderCounter,
name = name,
type = FolderType.REGULAR,
isLocalOnly = false,
),
isInTopGroup = true,
unreadMessageCount = 0,
starredMessageCount = 0,
pathDelimiter = pathDelimiter,
)
}
}

View file

@ -0,0 +1,14 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase
import kotlinx.coroutines.flow.Flow
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract.DrawerConfig
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract.DrawerConfigLoader
import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract.UseCase
internal class GetDrawerConfig(
private val configLoader: DrawerConfigLoader,
) : UseCase.GetDrawerConfig {
override operator fun invoke(): Flow<DrawerConfig> {
return configLoader.loadDrawerConfigFlow()
}
}

View file

@ -0,0 +1,17 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract.DrawerConfigWriter
import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract.UseCase
internal class SaveDrawerConfig(
private val drawerConfigWriter: DrawerConfigWriter,
) : UseCase.SaveDrawerConfig {
override fun invoke(drawerConfig: NavigationDrawerExternalContract.DrawerConfig): Flow<Unit> {
return flow {
emit(drawerConfigWriter.writeDrawerConfig(drawerConfig))
}
}
}

View file

@ -0,0 +1,41 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase
import android.content.Context
import app.k9mail.legacy.message.controller.MessagingControllerMailChecker
import app.k9mail.legacy.message.controller.SimpleMessagingListener
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import net.thunderbird.core.android.account.AccountManager
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract.UseCase
internal class SyncAccount(
private val accountManager: AccountManager,
private val messagingController: MessagingControllerMailChecker,
private val coroutineContext: CoroutineContext = Dispatchers.IO,
) : UseCase.SyncAccount {
override fun invoke(accountUuid: String): Flow<Result<Unit>> = callbackFlow {
val listener = object : SimpleMessagingListener() {
override fun checkMailFinished(context: Context?, account: LegacyAccount?) {
trySend(Result.success(Unit))
close()
}
}
val account = accountManager.getAccount(accountUuid)
messagingController.checkMail(
account = account,
ignoreLastCheckedTime = true,
useManualWakeLock = true,
notify = true,
listener = listener,
)
awaitClose()
}.flowOn(coroutineContext)
}

View file

@ -0,0 +1,37 @@
package net.thunderbird.feature.navigation.drawer.dropdown.domain.usecase
import android.content.Context
import app.k9mail.legacy.message.controller.MessagingControllerMailChecker
import app.k9mail.legacy.message.controller.SimpleMessagingListener
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract.UseCase
class SyncAllAccounts(
private val messagingController: MessagingControllerMailChecker,
private val coroutineContext: CoroutineContext = Dispatchers.IO,
) : UseCase.SyncAllAccounts {
override fun invoke(): Flow<Result<Unit>> = callbackFlow {
val listener = object : SimpleMessagingListener() {
override fun checkMailFinished(context: Context?, account: LegacyAccount?) {
trySend(Result.success(Unit))
close()
}
}
messagingController.checkMail(
account = null,
ignoreLastCheckedTime = true,
useManualWakeLock = true,
notify = true,
listener = listener,
)
awaitClose()
}.flowOn(coroutineContext)
}

View file

@ -0,0 +1,154 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.DividerHorizontal
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.core.ui.compose.common.modifier.testTagAsResourceId
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerContract.Event
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerContract.State
import net.thunderbird.feature.navigation.drawer.dropdown.ui.account.AccountList
import net.thunderbird.feature.navigation.drawer.dropdown.ui.account.AccountView
import net.thunderbird.feature.navigation.drawer.dropdown.ui.account.getDisplayCutOutHorizontalInsetPadding
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.DRAWER_WIDTH
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.getAdditionalWidth
import net.thunderbird.feature.navigation.drawer.dropdown.ui.folder.FolderList
import net.thunderbird.feature.navigation.drawer.dropdown.ui.setting.AccountSettingList
import net.thunderbird.feature.navigation.drawer.dropdown.ui.setting.FolderSettingList
@Composable
internal fun DrawerContent(
state: State,
onEvent: (Event) -> Unit,
modifier: Modifier = Modifier,
) {
val additionalWidth = getAdditionalWidth()
Surface(
modifier = modifier
.width(DRAWER_WIDTH + additionalWidth)
.fillMaxHeight()
.testTagAsResourceId("DrawerContent"),
color = MainTheme.colors.surfaceContainerLow,
) {
val selectedAccount = state.accounts.firstOrNull { it.id == state.selectedAccountId }
val horizontalInsetPadding = getDisplayCutOutHorizontalInsetPadding()
Column(
modifier = Modifier
.windowInsetsPadding(WindowInsets.safeDrawing)
.windowInsetsPadding(horizontalInsetPadding),
) {
selectedAccount?.let {
AccountView(
account = selectedAccount,
onClick = { onEvent(Event.OnAccountSelectorClick) },
showAccountSelection = state.showAccountSelection,
)
DividerHorizontal()
}
AnimatedContent(
targetState = state.showAccountSelection,
label = "AccountSelectorVisibility",
transitionSpec = {
if (targetState) {
slideInVertically { -it } togetherWith slideOutVertically { it }
} else {
slideInVertically { it } togetherWith slideOutVertically { -it }
}
},
) { targetState ->
if (targetState) {
AccountContent(
state = state,
onEvent = onEvent,
selectedAccount = selectedAccount,
)
} else {
FolderContent(
state = state,
onEvent = onEvent,
)
}
}
}
}
}
@Composable
private fun AccountContent(
state: State,
onEvent: (Event) -> Unit,
selectedAccount: DisplayAccount?,
) {
Surface(
color = MainTheme.colors.surfaceContainerLow,
) {
Column(
modifier = Modifier.fillMaxSize(),
) {
AccountList(
accounts = state.accounts,
selectedAccount = selectedAccount,
onAccountClick = { onEvent(Event.OnAccountClick(it)) },
showStarredCount = state.config.showStarredCount,
modifier = Modifier.weight(1f),
)
DividerHorizontal()
AccountSettingList(
onAddAccountClick = { onEvent(Event.OnAddAccountClick) },
onSyncAllAccountsClick = { onEvent(Event.OnSyncAllAccounts) },
)
}
}
}
@Composable
private fun FolderContent(
state: State,
onEvent: (Event) -> Unit,
) {
val isUnifiedAccount = remember(state.selectedAccountId) {
state.accounts.any { it.id == state.selectedAccountId && it is UnifiedDisplayAccount }
}
Surface(
color = MainTheme.colors.surfaceContainerLow,
) {
Column(
modifier = Modifier.fillMaxSize(),
) {
FolderList(
rootFolder = state.rootFolder,
selectedFolder = state.selectedFolder,
onFolderClick = { folder ->
onEvent(Event.OnFolderClick(folder))
},
showStarredCount = state.config.showStarredCount,
modifier = Modifier.weight(1f),
)
DividerHorizontal()
FolderSettingList(
onManageFoldersClick = { onEvent(Event.OnManageFoldersClick) },
onSettingsClick = { onEvent(Event.OnSettingsClick) },
isUnifiedAccount = isUnifiedAccount,
)
}
}
}

View file

@ -0,0 +1,62 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui
import androidx.compose.runtime.Stable
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract.DrawerConfig
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayTreeFolder
internal interface DrawerContract {
interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
@Stable
data class State(
val config: DrawerConfig = DrawerConfig(
showUnifiedFolders = false,
showStarredCount = false,
showAccountSelector = true,
),
val accounts: ImmutableList<DisplayAccount> = persistentListOf(),
val selectedAccountId: String? = null,
val rootFolder: DisplayTreeFolder = DisplayTreeFolder(
displayFolder = null,
displayName = null,
totalUnreadCount = 0,
totalStarredCount = 0,
children = persistentListOf(),
),
val folders: ImmutableList<DisplayFolder> = persistentListOf(),
val selectedFolderId: String? = null,
val selectedFolder: DisplayFolder? = null,
val showAccountSelection: Boolean = false,
val isLoading: Boolean = false,
)
sealed interface Event {
data class SelectAccount(val accountId: String?) : Event
data class SelectFolder(val folderId: String?) : Event
data class OnAccountClick(val account: DisplayAccount) : Event
data class OnAccountViewClick(val account: DisplayAccount) : Event
data class OnFolderClick(val folder: DisplayFolder) : Event
data object OnAccountSelectorClick : Event
data object OnManageFoldersClick : Event
data object OnSettingsClick : Event
data object OnSyncAccount : Event
data object OnSyncAllAccounts : Event
data object OnAddAccountClick : Event
}
sealed interface Effect {
data class OpenAccount(val accountId: String) : Effect
data class OpenFolder(val accountId: String, val folderId: Long) : Effect
data object OpenUnifiedFolder : Effect
data object OpenManageFolders : Effect
data object OpenSettings : Effect
data object OpenAddAccount : Effect
data object CloseDrawer : Effect
}
}

View file

@ -0,0 +1,76 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import app.k9mail.core.ui.compose.common.mvi.observe
import app.k9mail.core.ui.compose.designsystem.molecule.PullToRefreshBox
import net.thunderbird.core.featureflag.FeatureFlagKey
import net.thunderbird.core.featureflag.FeatureFlagProvider
import net.thunderbird.core.featureflag.FeatureFlagResult
import net.thunderbird.feature.navigation.drawer.dropdown.FolderDrawerState
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerContract.Effect
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerContract.Event
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerContract.ViewModel
import net.thunderbird.feature.navigation.drawer.siderail.ui.SideRailDrawerContent
import org.koin.androidx.compose.koinViewModel
@Suppress("LongParameterList")
@Composable
internal fun DrawerView(
drawerState: FolderDrawerState,
openAccount: (accountId: String) -> Unit,
openFolder: (accountId: String, folderId: Long) -> Unit,
openUnifiedFolder: () -> Unit,
openManageFolders: () -> Unit,
openSettings: () -> Unit,
openAddAccount: () -> Unit,
closeDrawer: () -> Unit,
featureFlagProvider: FeatureFlagProvider,
viewModel: ViewModel = koinViewModel<DrawerViewModel>(),
) {
val (state, dispatch) = viewModel.observe { effect ->
when (effect) {
is Effect.OpenAccount -> openAccount(effect.accountId)
is Effect.OpenFolder -> openFolder(
effect.accountId,
effect.folderId,
)
Effect.OpenUnifiedFolder -> openUnifiedFolder()
is Effect.OpenManageFolders -> openManageFolders()
is Effect.OpenSettings -> openSettings()
Effect.OpenAddAccount -> openAddAccount()
Effect.CloseDrawer -> closeDrawer()
}
}
val isDropdownDrawerEnabled = remember {
featureFlagProvider.provide(FeatureFlagKey("enable_dropdown_drawer_ui")) == FeatureFlagResult.Enabled
}
LaunchedEffect(drawerState.selectedAccountUuid) {
dispatch(Event.SelectAccount(drawerState.selectedAccountUuid))
}
LaunchedEffect(drawerState.selectedFolderId) {
dispatch(Event.SelectFolder(drawerState.selectedFolderId))
}
PullToRefreshBox(
isRefreshing = state.value.isLoading,
onRefresh = { dispatch(Event.OnSyncAccount) },
) {
if (isDropdownDrawerEnabled) {
DrawerContent(
state = state.value,
onEvent = { dispatch(it) },
)
} else {
SideRailDrawerContent(
state = state.value,
onEvent = { dispatch(it) },
)
}
}
}

View file

@ -0,0 +1,292 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui
import androidx.lifecycle.viewModelScope
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import net.thunderbird.feature.navigation.drawer.dropdown.domain.DomainContract.UseCase
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayTreeFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.MailDisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerContract.Effect
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerContract.Event
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerContract.State
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerContract.ViewModel
/**
* Delay before closing the drawer to avoid the drawer being closed immediately and give time
* for the ripple effect to finish.
*/
private const val DRAWER_CLOSE_DELAY = 250L
private const val ACCOUNT_CLOSE_DELAY = 150L
@Suppress("MagicNumber", "TooManyFunctions")
internal class DrawerViewModel(
private val getDrawerConfig: UseCase.GetDrawerConfig,
private val saveDrawerConfig: UseCase.SaveDrawerConfig,
private val getDisplayAccounts: UseCase.GetDisplayAccounts,
private val getDisplayFoldersForAccount: UseCase.GetDisplayFoldersForAccount,
private val getDisplayTreeFolder: UseCase.GetDisplayTreeFolder,
private val syncAccount: UseCase.SyncAccount,
private val syncAllAccounts: UseCase.SyncAllAccounts,
private val maxNestingLevel: Int = 2,
initialState: State = State(),
) : BaseViewModel<State, Event, Effect>(
initialState = initialState,
),
ViewModel {
init {
viewModelScope.launch {
getDrawerConfig().collectLatest { config ->
updateState {
it.copy(config = config)
}
}
}
viewModelScope.launch {
loadAccounts()
}
viewModelScope.launch {
loadFolders()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun loadAccounts() {
state.map { it.config.showUnifiedFolders }
.distinctUntilChanged()
.flatMapLatest { showUnifiedFolders ->
getDisplayAccounts(showUnifiedFolders)
}.collectLatest { accounts ->
updateAccounts(accounts)
}
}
private fun updateAccounts(accounts: List<DisplayAccount>) {
val selectedAccount = accounts.find { it.id == state.value.selectedAccountId }
?: accounts.firstOrNull()
updateState {
it.copy(
accounts = accounts.toImmutableList(),
selectedAccountId = selectedAccount?.id,
)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun loadFolders() {
state.map {
it.selectedAccountId
}.filterNotNull()
.distinctUntilChanged()
.flatMapLatest { accountId ->
getDisplayFoldersForAccount(accountId)
}.collect { folders ->
updateFolders(folders, getDisplayTreeFolder(folders, maxNestingLevel))
}
}
private fun updateFolders(displayFolders: List<DisplayFolder>, rootFolder: DisplayTreeFolder) {
// First try to find the folder in the flat list
var selectedFolder = displayFolders.find {
it.id == state.value.selectedFolderId
}
// If not found, try to find it in the tree hierarchy
if (selectedFolder == null) {
selectedFolder = findFolderById(rootFolder, state.value.selectedFolderId)
}
// If still not found, default to the first folder
if (selectedFolder == null) {
selectedFolder = displayFolders.firstOrNull() ?: rootFolder.children.firstOrNull()?.displayFolder
}
updateState {
it.copy(
rootFolder = rootFolder,
folders = displayFolders.toImmutableList(),
selectedFolderId = selectedFolder?.id,
selectedFolder = selectedFolder,
)
}
}
/**
* Recursively searches for a folder with the given ID in the DisplayTreeFolder hierarchy.
*/
private fun findFolderById(treeFolder: DisplayTreeFolder, folderId: String?): DisplayFolder? {
if (folderId == null) return null
return if (treeFolder.displayFolder?.id == folderId) {
treeFolder.displayFolder
} else {
// Recursively search in children
var folder: DisplayFolder? = null
for (child in treeFolder.children) {
val found = findFolderById(child, folderId)
if (found != null) {
folder = found
break
}
}
folder
}
}
override fun event(event: Event) {
when (event) {
is Event.SelectAccount -> selectAccount(event.accountId)
is Event.SelectFolder -> selectFolder(event.folderId)
is Event.OnAccountClick -> openAccount(event.account)
is Event.OnFolderClick -> openFolder(event.folder)
is Event.OnAccountViewClick -> {
openAccount(
state.value.accounts.nextOrFirst(event.account),
)
}
Event.OnAccountSelectorClick -> {
viewModelScope.launch {
saveDrawerConfig(
state.value.config.copy(showAccountSelector = state.value.config.showAccountSelector.not()),
).launchIn(viewModelScope)
delay(ACCOUNT_CLOSE_DELAY)
updateState {
it.copy(showAccountSelection = it.showAccountSelection.not())
}
}
}
Event.OnManageFoldersClick -> emitEffect(Effect.OpenManageFolders)
Event.OnSettingsClick -> emitEffect(Effect.OpenSettings)
Event.OnSyncAccount -> onSyncAccount()
Event.OnSyncAllAccounts -> onSyncAllAccounts()
Event.OnAddAccountClick -> emitEffect(Effect.OpenAddAccount)
}
}
private fun selectAccount(accountId: String?) {
if (accountId != state.value.selectedAccountId) {
viewModelScope.launch {
updateState {
it.copy(
selectedAccountId = accountId,
)
}
delay(ACCOUNT_CLOSE_DELAY)
updateState {
it.copy(
showAccountSelection = false,
)
}
}
}
}
private fun selectFolder(folderId: String?) {
// Find the folder with the given ID
val folder = folderId?.let {
state.value.folders.find { it.id == folderId }
// If not found, try to find it in the tree hierarchy
?: findFolderById(state.value.rootFolder, folderId)
}
updateState {
it.copy(
selectedFolderId = folderId,
selectedFolder = folder,
)
}
}
private fun openAccount(account: DisplayAccount?) {
if (account != null) {
emitEffect(Effect.OpenAccount(account.id))
}
}
private fun ImmutableList<DisplayAccount>.nextOrFirst(account: DisplayAccount): DisplayAccount? {
val index = indexOf(account)
return if (index == -1) {
null
} else if (index == size - 1) {
get(0)
} else {
get(index + 1)
}
}
private fun openFolder(folder: DisplayFolder) {
// Update the selected folder ID in the state
selectFolder(folder.id)
if (folder is MailDisplayFolder) {
if (folder.accountId != null) {
emitEffect(
Effect.OpenFolder(
accountId = folder.accountId,
folderId = folder.folder.id,
),
)
}
} else if (folder is UnifiedDisplayFolder) {
emitEffect(Effect.OpenUnifiedFolder)
}
viewModelScope.launch {
delay(DRAWER_CLOSE_DELAY)
emitEffect(Effect.CloseDrawer)
}
}
private fun onSyncAccount() {
if (state.value.isLoading || state.value.selectedAccountId == null) return
viewModelScope.launch {
updateState {
it.copy(isLoading = true)
}
state.value.selectedAccountId?.let { syncAccount(it).collect() }
updateState {
it.copy(isLoading = false)
}
}
}
private fun onSyncAllAccounts() {
if (state.value.isLoading) return
viewModelScope.launch {
updateState {
it.copy(isLoading = true)
}
syncAllAccounts().collect()
updateState {
it.copy(isLoading = false)
}
}
}
}

View file

@ -0,0 +1,76 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.account
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.designsystem.atom.text.TextLabelSmall
import app.k9mail.core.ui.compose.theme2.ColorRoles
import net.thunderbird.feature.account.avatar.ui.Avatar
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.getDisplayAccountColor
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.getDisplayAccountName
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.labelForCount
@Composable
internal fun AccountAvatar(
account: DisplayAccount,
selected: Boolean,
modifier: Modifier = Modifier,
onClick: ((DisplayAccount) -> Unit)? = null,
) {
val name = getDisplayAccountName(account)
val color = getDisplayAccountColor(account)
val accountColor = rememberCalculatedAccountColor(color)
val accountColorRoles = rememberCalculatedAccountColorRoles(accountColor)
Box(
modifier = modifier,
contentAlignment = Alignment.BottomEnd,
) {
Avatar(
color = accountColor,
name = name,
onClick = onClick?.let { { onClick(account) } },
selected = selected,
)
UnreadBadge(
unreadCount = account.unreadMessageCount,
accountColorRoles = accountColorRoles,
)
}
}
@Composable
private fun UnreadBadge(
unreadCount: Int,
accountColorRoles: ColorRoles,
modifier: Modifier = Modifier,
) {
if (unreadCount > 0) {
val resources = LocalContext.current.resources
Surface(
color = accountColorRoles.accent,
shape = CircleShape,
modifier = modifier,
) {
TextLabelSmall(
text = labelForCount(
count = unreadCount,
resources = resources,
),
color = accountColorRoles.onAccent,
modifier = Modifier.padding(
horizontal = 3.dp,
vertical = 2.dp,
),
)
}
}
}

View file

@ -0,0 +1,42 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.account
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.theme2.MainTheme
import kotlinx.collections.immutable.ImmutableList
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
@Composable
internal fun AccountList(
accounts: ImmutableList<DisplayAccount>,
selectedAccount: DisplayAccount?,
onAccountClick: (DisplayAccount) -> Unit,
showStarredCount: Boolean,
modifier: Modifier = Modifier,
) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = modifier
.fillMaxWidth(),
contentPadding = PaddingValues(vertical = MainTheme.spacings.default),
) {
items(
items = accounts,
key = { account -> account.id },
) { account ->
AccountListItem(
account = account,
onClick = { onAccountClick(account) },
selected = selectedAccount == account,
showStarredCount = showStarredCount,
)
}
}
}

View file

@ -0,0 +1,82 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.account
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.core.ui.compose.designsystem.atom.text.TextBodyLarge
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
import app.k9mail.core.ui.compose.designsystem.organism.drawer.NavigationDrawerItem
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.feature.account.avatar.ui.AvatarOutlined
import net.thunderbird.feature.account.avatar.ui.AvatarSize
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.MailDisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.getDisplayAccountColor
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.getDisplayAccountName
@Composable
internal fun AccountListItem(
account: DisplayAccount,
onClick: (DisplayAccount) -> Unit,
selected: Boolean,
showStarredCount: Boolean,
modifier: Modifier = Modifier,
) {
val color = getDisplayAccountColor(account)
val name = getDisplayAccountName(account)
NavigationDrawerItem(
label = { AccountLabel(account = account) },
selected = selected,
onClick = { onClick(account) },
modifier = modifier.fillMaxWidth()
.height(MainTheme.sizes.large),
icon = {
AvatarOutlined(
color = color,
name = name,
size = AvatarSize.MEDIUM,
)
},
badge = {
AccountListItemBadge(
unreadCount = account.unreadMessageCount,
starredCount = account.starredMessageCount,
showStarredCount = showStarredCount,
)
},
)
}
@Composable
private fun AccountLabel(
account: DisplayAccount,
modifier: Modifier = Modifier,
) {
val name = getDisplayAccountName(account)
Column(
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.half),
modifier = modifier.fillMaxWidth(),
) {
TextBodyLarge(
text = buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(name)
}
},
)
if (account is MailDisplayAccount && account.name != account.email) {
TextBodyMedium(
text = account.email,
)
}
}
}

View file

@ -0,0 +1,65 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.account
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
import app.k9mail.core.ui.compose.designsystem.organism.drawer.NavigationDrawerItemBadge
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.core.ui.compose.designsystem.atom.icon.filled.Star
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.labelForCount
@Composable
internal fun AccountListItemBadge(
unreadCount: Int,
starredCount: Int,
showStarredCount: Boolean,
modifier: Modifier = Modifier,
) {
AccountCountAndStarredBadge(
unreadCount = unreadCount,
starredCount = starredCount,
showStarredCount = showStarredCount,
modifier = modifier,
)
}
@Composable
private fun AccountCountAndStarredBadge(
unreadCount: Int,
starredCount: Int,
showStarredCount: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.End,
) {
val resources = LocalContext.current.resources
if (unreadCount > 0) {
NavigationDrawerItemBadge(
label = labelForCount(
count = unreadCount,
resources = resources,
),
imageVector = if (showStarredCount) Icons.Filled.Dot else null,
)
}
if (showStarredCount && starredCount > 0) {
Spacer(modifier = Modifier.Companion.width(MainTheme.spacings.half))
NavigationDrawerItemBadge(
label = labelForCount(
count = starredCount,
resources = resources,
),
imageVector = Icons.Filled.Star,
)
}
}
}

View file

@ -0,0 +1,148 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.account
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
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 androidx.compose.ui.unit.LayoutDirection
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 net.thunderbird.feature.account.avatar.ui.AvatarOutlined
import net.thunderbird.feature.account.avatar.ui.AvatarSize
import net.thunderbird.feature.navigation.drawer.dropdown.R
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.MailDisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.AnimatedExpandIcon
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.getDisplayAccountColor
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.getDisplayAccountName
@Composable
internal fun AccountView(
account: DisplayAccount,
onClick: () -> Unit,
showAccountSelection: Boolean,
modifier: Modifier = Modifier,
) {
AccountLayout(
onClick = onClick,
modifier = modifier,
) {
if (showAccountSelection) {
AccountSelectionView()
} else {
AccountSelectedView(
account = account,
)
}
AnimatedExpandIcon(
isExpanded = showAccountSelection,
modifier = Modifier.padding(end = MainTheme.spacings.double),
tint = MainTheme.colors.onSurfaceVariant,
)
}
}
@Composable
private fun RowScope.AccountSelectedView(
account: DisplayAccount,
) {
val color = getDisplayAccountColor(account)
val name = getDisplayAccountName(account)
AvatarOutlined(
color = color,
name = name,
size = AvatarSize.MEDIUM,
)
Column(
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.half),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
) {
TextBodyLarge(
text = buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(name)
}
},
)
if (account is MailDisplayAccount && account.name != account.email) {
TextBodyMedium(
text = account.email,
)
}
}
}
@Composable
private fun RowScope.AccountSelectionView() {
TextBodyLarge(
text = buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(stringResource(R.string.navigation_drawer_dropdown_avount_view_selection_title))
}
},
modifier = Modifier
.fillMaxWidth()
.weight(1f),
)
}
@Composable
private fun AccountLayout(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit,
) {
val horizontalInsetPadding = getDisplayCutOutHorizontalInsetPadding()
Box(
modifier = modifier
.windowInsetsPadding(horizontalInsetPadding)
.clickable(onClick = onClick)
.padding(
top = MainTheme.spacings.default,
start = MainTheme.spacings.triple,
end = MainTheme.spacings.double,
bottom = MainTheme.spacings.default,
),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(MainTheme.sizes.large),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.double),
) {
content()
}
}
}
@Composable
fun getDisplayCutOutHorizontalInsetPadding(): WindowInsets {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
return WindowInsets.displayCutout.only(if (isRtl) WindowInsetsSides.Right else WindowInsetsSides.Left)
}

View file

@ -0,0 +1,31 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.account
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import app.k9mail.core.ui.compose.theme2.MainTheme
import app.k9mail.core.ui.compose.theme2.toHarmonizedColor
/**
* Calculates the account color based on the provided account color and surface color.
*
* If the account color is unspecified, it returns the fallback color.
* Otherwise, it harmonizes the account color with the surface color.
*
* @param accountColor The color of the account.
* @param fallbackColor The fallback color to use if the account color is unspecified.
*/
@Composable
internal fun rememberCalculatedAccountColor(
accountColor: Color,
fallbackColor: Color = MainTheme.colors.primary,
): Color {
val surfaceColor = MainTheme.colors.surface
return remember(accountColor, surfaceColor, fallbackColor) {
if (accountColor == Color.Unspecified) {
fallbackColor
} else {
accountColor.toHarmonizedColor(surfaceColor)
}
}
}

View file

@ -0,0 +1,27 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.account
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import app.k9mail.core.ui.compose.theme2.ColorRoles
import app.k9mail.core.ui.compose.theme2.toColorRoles
/**
* Calculates the color roles for the given account color.
*
* This function is used to derive the color roles for an account based on its color and
* use remember to avoid unnecessary recomputations.
*
* @param accountColor The color of the account.
*/
@Composable
internal fun rememberCalculatedAccountColorRoles(
accountColor: Color,
): ColorRoles {
val context = LocalContext.current
return remember(accountColor) {
accountColor.toColorRoles(context)
}
}

View file

@ -0,0 +1,30 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.common
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icon
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
@Composable
internal fun AnimatedExpandIcon(
isExpanded: Boolean,
modifier: Modifier = Modifier,
tint: Color? = null,
) {
val rotationAngle by animateFloatAsState(
targetValue = if (isExpanded) 180f else 0f,
label = "rotationAngle",
)
Icon(
imageVector = Icons.Outlined.KeyboardArrowDown,
contentDescription = null,
tint = tint,
modifier = modifier
.rotate(rotationAngle),
)
}

View file

@ -0,0 +1,38 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.common
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.feature.navigation.drawer.dropdown.R
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.MailDisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayAccount
@Composable
internal fun getDisplayAccountColor(account: DisplayAccount): Color {
return when (account) {
is UnifiedDisplayAccount -> {
MainTheme.colors.onSurfaceVariant
}
is MailDisplayAccount -> {
Color(account.color)
}
else -> throw IllegalArgumentException("Unknown account type: ${account::class.java.simpleName}")
}
}
@Composable
internal fun getDisplayAccountName(account: DisplayAccount): String {
return when (account) {
is UnifiedDisplayAccount -> {
stringResource(R.string.navigation_drawer_dropdown_unified_account_title)
}
is MailDisplayAccount -> {
account.name
}
else -> throw IllegalArgumentException("Unknown account type: ${account::class.java.simpleName}")
}
}

View file

@ -0,0 +1,30 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.common
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
// As long as we use DrawerLayout, we don't have to worry about screens narrower than DRAWER_WIDTH. DrawerLayout will
// automatically limit the width of the content view so there's still room for a scrim with minimum tap width.
internal val DRAWER_WIDTH = 360.dp
@Composable
internal fun getAdditionalWidth(): Dp {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
return if (isRtl) {
WindowInsets.displayCutout.getRight(density = density, layoutDirection = layoutDirection)
} else {
WindowInsets.displayCutout.getLeft(density = density, layoutDirection = layoutDirection)
}.pxToDp()
}
@Composable
private fun Int.pxToDp() = with(LocalDensity.current) { this@pxToDp.toDp() }

View file

@ -0,0 +1,22 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.common
import android.content.res.Resources
import net.thunderbird.feature.navigation.drawer.dropdown.R
@Suppress("MagicNumber")
internal fun labelForCount(
count: Int,
resources: Resources,
) = when {
count in 1..99 -> "$count"
count in 100..1000 -> resources.getString(
R.string.navigation_drawer_dropdown_folder_item_badge_count_greater_than_99,
)
count > 1000 -> resources.getString(
R.string.navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000,
)
else -> ""
}

View file

@ -0,0 +1,51 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.folder
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import app.k9mail.core.ui.compose.theme2.MainTheme
import app.k9mail.legacy.ui.folder.FolderNameFormatter
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayTreeFolder
@Composable
internal fun FolderList(
rootFolder: DisplayTreeFolder,
selectedFolder: DisplayFolder?,
onFolderClick: (DisplayFolder) -> Unit,
showStarredCount: Boolean,
modifier: Modifier = Modifier,
) {
val resources = LocalContext.current.resources
val folderNameFormatter = remember { FolderNameFormatter(resources) }
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = modifier.fillMaxWidth(),
contentPadding = PaddingValues(vertical = MainTheme.spacings.default),
) {
items(
items = rootFolder.children,
key = { it.displayFolder?.id ?: '0' },
) { folder ->
val currentDisplayFolder = folder.displayFolder
FolderListItem(
displayFolder = requireNotNull(currentDisplayFolder) {
"Null DisplayFolder for folder ${folder.displayName}"
},
treeFolder = folder,
showStarredCount = showStarredCount,
onClick = onFolderClick,
folderNameFormatter = folderNameFormatter,
selectedFolderId = selectedFolder?.id,
)
}
}
}

View file

@ -0,0 +1,201 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.folder
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
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.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
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.TextLabelLarge
import app.k9mail.core.ui.compose.designsystem.organism.drawer.NavigationDrawerItem
import app.k9mail.core.ui.compose.theme2.MainTheme
import app.k9mail.legacy.ui.folder.FolderNameFormatter
import net.thunderbird.feature.mail.folder.api.FolderType
import net.thunderbird.feature.navigation.drawer.dropdown.R
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayTreeFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.MailDisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolder
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.UnifiedDisplayFolderType
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.AnimatedExpandIcon
@Composable
internal fun FolderListItem(
displayFolder: DisplayFolder,
onClick: (DisplayFolder) -> Unit,
showStarredCount: Boolean,
folderNameFormatter: FolderNameFormatter,
selectedFolderId: String?,
modifier: Modifier = Modifier,
treeFolder: DisplayTreeFolder? = null,
parentPrefix: String? = "",
indentationLevel: Int = 1,
) {
val isExpanded = rememberSaveable { mutableStateOf(false) }
var unreadCount = displayFolder.unreadMessageCount
var starredCount = displayFolder.starredMessageCount
if (treeFolder !== null && !isExpanded.value) {
unreadCount = treeFolder.totalUnreadCount
starredCount = treeFolder.totalStarredCount
}
Column(
modifier = modifier
.fillMaxWidth()
.animateContentSize(),
) {
NavigationDrawerItem(
label = {
NavigationDrawerLabel(
label = mapFolderName(displayFolder, folderNameFormatter, parentPrefix),
expandableState = if (treeFolder !== null && treeFolder.children.isNotEmpty()) isExpanded else null,
badge = {
FolderListItemBadge(
unreadCount = unreadCount,
starredCount = starredCount,
showStarredCount = showStarredCount,
)
},
)
},
selected = selectedFolderId == displayFolder.id,
onClick = {
when (displayFolder) {
is MailDisplayFolder if displayFolder.accountId == null -> isExpanded.value = !isExpanded.value
else -> onClick(displayFolder)
}
},
modifier = Modifier.fillMaxWidth(),
icon = { Icon(imageVector = mapFolderIcon(displayFolder)) },
)
// Managing children
if (!isExpanded.value) return
if (treeFolder === null) return
for (child in treeFolder.children) {
val displayParent = treeFolder.displayFolder
val displayChild = child.displayFolder
if (displayChild == null) continue
FolderListItem(
displayFolder = displayChild,
selectedFolderId = selectedFolderId,
showStarredCount = showStarredCount,
onClick = onClick,
folderNameFormatter = folderNameFormatter,
modifier = Modifier
.fillMaxWidth()
.padding(start = MainTheme.spacings.double * indentationLevel),
treeFolder = child,
parentPrefix = if (displayParent is MailDisplayFolder) displayParent.folder.name else null,
indentationLevel = indentationLevel + 1,
)
}
}
}
@Composable
private fun NavigationDrawerLabel(
label: String,
badge: @Composable () -> Unit,
modifier: Modifier = Modifier,
expandableState: MutableState<Boolean>? = null,
) {
Row(
modifier = modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
TextLabelLarge(
text = label,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
modifier = Modifier.weight(1f),
)
if (expandableState?.value != null) {
Box(
modifier = Modifier
.size(MainTheme.sizes.iconAvatar)
.padding(
start = MainTheme.spacings.quarter,
end = MainTheme.spacings.quarter,
)
.clip(CircleShape)
.clickable(onClick = { expandableState.value = !expandableState.value }),
contentAlignment = Alignment.Center,
) {
AnimatedExpandIcon(
isExpanded = expandableState.value,
)
}
}
badge()
}
}
@Composable
private fun mapFolderName(
displayFolder: DisplayFolder,
folderNameFormatter: FolderNameFormatter,
parentPrefix: String? = "",
): String {
return when (displayFolder) {
is MailDisplayFolder ->
folderNameFormatter
.displayName(displayFolder.folder)
.removePrefix("$parentPrefix${displayFolder.pathDelimiter}")
is UnifiedDisplayFolder -> mapUnifiedFolderName(displayFolder)
else -> throw IllegalArgumentException("Unknown display folder: $displayFolder")
}
}
@Composable
private fun mapUnifiedFolderName(folder: UnifiedDisplayFolder): String {
return when (folder.unifiedType) {
UnifiedDisplayFolderType.INBOX -> stringResource(R.string.navigation_drawer_dropdown_unified_inbox_title)
}
}
private fun mapFolderIcon(folder: DisplayFolder): ImageVector {
return when (folder) {
is MailDisplayFolder -> mapDisplayAccountFolderIcon(folder)
is UnifiedDisplayFolder -> mapDisplayUnifiedFolderIcon(folder)
else -> throw IllegalArgumentException("Unknown display folder type: $folder")
}
}
private fun mapDisplayAccountFolderIcon(folder: MailDisplayFolder): ImageVector {
return when (folder.folder.type) {
FolderType.INBOX -> Icons.Outlined.Inbox
FolderType.OUTBOX -> Icons.Outlined.Outbox
FolderType.SENT -> Icons.Outlined.Send
FolderType.TRASH -> Icons.Outlined.Delete
FolderType.DRAFTS -> Icons.Outlined.Drafts
FolderType.ARCHIVE -> Icons.Outlined.Archive
FolderType.SPAM -> Icons.Outlined.Report
FolderType.REGULAR -> Icons.Outlined.Folder
}
}
private fun mapDisplayUnifiedFolderIcon(folder: UnifiedDisplayFolder): ImageVector {
when (folder.unifiedType) {
UnifiedDisplayFolderType.INBOX -> return Icons.Outlined.AllInbox
}
}

View file

@ -0,0 +1,65 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.folder
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
import app.k9mail.core.ui.compose.designsystem.organism.drawer.NavigationDrawerItemBadge
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.core.ui.compose.designsystem.atom.icon.filled.Star
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.labelForCount
@Composable
internal fun FolderListItemBadge(
unreadCount: Int,
starredCount: Int,
showStarredCount: Boolean,
modifier: Modifier = Modifier,
) {
FolderCountAndStarredBadge(
unreadCount = unreadCount,
starredCount = starredCount,
showStarredCount = showStarredCount,
modifier = modifier,
)
}
@Composable
private fun FolderCountAndStarredBadge(
unreadCount: Int,
starredCount: Int,
showStarredCount: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.End,
) {
val resources = LocalContext.current.resources
if (unreadCount > 0) {
NavigationDrawerItemBadge(
label = labelForCount(
count = unreadCount,
resources = resources,
),
imageVector = if (showStarredCount) Icons.Filled.Dot else null,
)
}
if (showStarredCount && starredCount > 0) {
Spacer(modifier = Modifier.width(MainTheme.spacings.half))
NavigationDrawerItemBadge(
label = labelForCount(
count = starredCount,
resources = resources,
),
imageVector = Icons.Filled.Star,
)
}
}
}

View file

@ -0,0 +1,38 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.setting
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.feature.navigation.drawer.dropdown.R
@Composable
internal fun AccountSettingList(
onAddAccountClick: () -> Unit,
onSyncAllAccountsClick: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingList(
modifier = modifier
.padding(vertical = MainTheme.spacings.default)
.fillMaxWidth(),
) {
item {
SettingListItem(
label = stringResource(id = R.string.navigation_drawer_dropdown_action_sync_all_accounts),
onClick = onSyncAllAccountsClick,
icon = Icons.Outlined.Sync,
)
}
item {
SettingListItem(
label = stringResource(id = R.string.navigation_drawer_dropdown_action_add_account),
onClick = onAddAccountClick,
icon = Icons.Outlined.Add,
)
}
}
}

View file

@ -0,0 +1,41 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.setting
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.feature.navigation.drawer.dropdown.R
@Composable
internal fun FolderSettingList(
onManageFoldersClick: () -> Unit,
onSettingsClick: () -> Unit,
isUnifiedAccount: Boolean,
modifier: Modifier = Modifier,
) {
SettingList(
modifier = modifier
.padding(vertical = MainTheme.spacings.default)
.fillMaxWidth(),
) {
if (isUnifiedAccount.not()) {
item {
SettingListItem(
label = stringResource(R.string.navigation_drawer_dropdown_action_manage_folders),
onClick = onManageFoldersClick,
icon = Icons.Outlined.FolderManaged,
)
}
}
item {
SettingListItem(
label = stringResource(id = R.string.navigation_drawer_dropdown_action_settings),
onClick = onSettingsClick,
icon = Icons.Outlined.Settings,
)
}
}
}

View file

@ -0,0 +1,28 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.setting
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.common.window.WindowSizeClass
import app.k9mail.core.ui.compose.common.window.getWindowSizeInfo
@Composable
internal fun SettingList(
modifier: Modifier = Modifier,
content: LazyGridScope.() -> Unit,
) {
val windowSizeInfo = getWindowSizeInfo()
val isLandscape = windowSizeInfo.screenWidth > windowSizeInfo.screenHeight
val useMultipleRows = isLandscape && windowSizeInfo.screenWidthSizeClass != WindowSizeClass.Compact
val rows = if (useMultipleRows) 2 else 1
LazyVerticalGrid(
columns = GridCells.Fixed(rows),
modifier = modifier,
) {
content()
}
}

View file

@ -0,0 +1,27 @@
package net.thunderbird.feature.navigation.drawer.dropdown.ui.setting
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icon
import app.k9mail.core.ui.compose.designsystem.organism.drawer.NavigationDrawerItem
@Composable
internal fun SettingListItem(
label: String,
onClick: () -> Unit,
icon: ImageVector,
modifier: Modifier = Modifier,
) {
NavigationDrawerItem(
label = label,
onClick = onClick,
modifier = modifier,
selected = false,
icon = {
Icon(
imageVector = icon,
)
},
)
}

View file

@ -0,0 +1,88 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.DividerHorizontal
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import net.thunderbird.core.ui.compose.common.modifier.testTagAsResourceId
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerContract.Event
import net.thunderbird.feature.navigation.drawer.dropdown.ui.DrawerContract.State
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.DRAWER_WIDTH
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.getAdditionalWidth
import net.thunderbird.feature.navigation.drawer.dropdown.ui.folder.FolderList
import net.thunderbird.feature.navigation.drawer.siderail.ui.account.SideRailAccountList
import net.thunderbird.feature.navigation.drawer.siderail.ui.account.SideRailAccountView
import net.thunderbird.feature.navigation.drawer.siderail.ui.setting.SideRailSettingList
@Composable
internal fun SideRailDrawerContent(
state: State,
onEvent: (Event) -> Unit,
modifier: Modifier = Modifier,
) {
val additionalWidth = getAdditionalWidth()
Surface(
modifier = modifier
.windowInsetsPadding(WindowInsets.statusBars)
.width(DRAWER_WIDTH + additionalWidth)
.fillMaxHeight()
.testTagAsResourceId("DrawerContent"),
) {
val selectedAccount = state.accounts.firstOrNull { it.id == state.selectedAccountId }
Column {
selectedAccount?.let {
SideRailAccountView(
account = selectedAccount,
onClick = { onEvent(Event.OnAccountViewClick(selectedAccount)) },
showAvatar = state.config.showAccountSelector,
)
DividerHorizontal()
}
Row {
AnimatedVisibility(
visible = state.config.showAccountSelector,
) {
SideRailAccountList(
accounts = state.accounts,
selectedAccount = selectedAccount,
onAccountClick = { onEvent(Event.OnAccountClick(it)) },
onSyncAllAccountsClick = { onEvent(Event.OnSyncAllAccounts) },
onSettingsClick = { onEvent(Event.OnSettingsClick) },
)
}
Column(
modifier = Modifier
.weight(1f)
.fillMaxSize(),
) {
FolderList(
rootFolder = state.rootFolder,
selectedFolder = state.folders.firstOrNull { it.id == state.selectedFolderId },
onFolderClick = { folder ->
onEvent(Event.OnFolderClick(folder))
},
showStarredCount = state.config.showStarredCount,
modifier = Modifier.weight(1f),
)
DividerHorizontal()
SideRailSettingList(
onAccountSelectorClick = { onEvent(Event.OnAccountSelectorClick) },
onManageFoldersClick = { onEvent(Event.OnManageFoldersClick) },
showAccountSelector = state.config.showAccountSelector,
)
}
}
}
}
}

View file

@ -0,0 +1,26 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui.account
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.feature.navigation.drawer.dropdown.ui.account.rememberCalculatedAccountColor
@Composable
internal fun SideRailAccountIndicator(
accountColor: Color,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier
.width(MainTheme.spacings.half)
.defaultMinSize(
minHeight = MainTheme.spacings.default,
),
color = rememberCalculatedAccountColor(accountColor),
shape = MainTheme.shapes.medium,
) {}
}

View file

@ -0,0 +1,83 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui.account
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
import app.k9mail.core.ui.compose.theme2.MainTheme
import kotlinx.collections.immutable.ImmutableList
import net.thunderbird.feature.navigation.drawer.dropdown.R
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.ui.account.getDisplayCutOutHorizontalInsetPadding
import net.thunderbird.feature.navigation.drawer.siderail.ui.setting.SideRailSettingItem
@Composable
internal fun SideRailAccountList(
accounts: ImmutableList<DisplayAccount>,
selectedAccount: DisplayAccount?,
onAccountClick: (DisplayAccount) -> Unit,
onSyncAllAccountsClick: () -> Unit,
onSettingsClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier,
color = MainTheme.colors.surfaceContainer,
) {
val horizontalInsetPadding = getDisplayCutOutHorizontalInsetPadding()
Column(
modifier = Modifier
.fillMaxHeight()
.windowInsetsPadding(WindowInsets.navigationBars)
.windowInsetsPadding(horizontalInsetPadding)
.width(MainTheme.sizes.large),
) {
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(vertical = MainTheme.spacings.default),
) {
items(
items = accounts,
key = { account -> account.id },
) { account ->
SideRailAccountListItem(
account = account,
onClick = { onAccountClick(account) },
selected = selectedAccount == account,
)
}
}
Column(
modifier = Modifier.padding(vertical = MainTheme.spacings.oneHalf),
) {
SideRailSettingItem(
icon = Icons.Outlined.Sync,
label = stringResource(id = R.string.navigation_drawer_dropdown_action_sync_all_accounts),
onClick = onSyncAllAccountsClick,
)
// Hack to compensate the column placement at an uneven coordinate, caused by the 1.dp divider.
Spacer(modifier = Modifier.height(7.dp))
SideRailSettingItem(
icon = Icons.Outlined.Settings,
label = stringResource(id = R.string.navigation_drawer_dropdown_action_settings),
onClick = onSettingsClick,
)
}
}
}
}

View file

@ -0,0 +1,31 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui.account
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.ui.account.AccountAvatar
@Composable
internal fun SideRailAccountListItem(
account: DisplayAccount,
onClick: (DisplayAccount) -> Unit,
selected: Boolean,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier.width(MainTheme.sizes.large)
.padding(vertical = MainTheme.spacings.half),
contentAlignment = Alignment.Center,
) {
AccountAvatar(
account = account,
onClick = onClick,
selected = selected,
)
}
}

View file

@ -0,0 +1,115 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui.account
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import app.k9mail.core.ui.compose.designsystem.atom.Surface
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 net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.MailDisplayAccount
import net.thunderbird.feature.navigation.drawer.dropdown.ui.account.AccountAvatar
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.getDisplayAccountColor
import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.getDisplayAccountName
@Suppress("LongMethod")
@Composable
internal fun SideRailAccountView(
account: DisplayAccount,
onClick: () -> Unit,
showAvatar: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = Modifier.fillMaxWidth()
.height(intrinsicSize = IntrinsicSize.Max),
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(visible = showAvatar) {
Surface(
color = MainTheme.colors.surfaceContainer,
modifier = Modifier.fillMaxHeight(),
) {
val horizontalInsetPadding = getDisplayCutOutHorizontalInsetPadding()
Box(
modifier = Modifier
.windowInsetsPadding(horizontalInsetPadding)
.width(MainTheme.sizes.large),
contentAlignment = Alignment.Center,
) {
AccountAvatar(
account = account,
onClick = null,
selected = false,
)
}
}
}
Row(
modifier = modifier
.clickable(onClick = onClick)
.height(intrinsicSize = IntrinsicSize.Max)
.fillMaxWidth()
.defaultMinSize(minHeight = MainTheme.sizes.large)
.padding(
top = MainTheme.spacings.double,
start = MainTheme.spacings.double,
end = MainTheme.spacings.triple,
bottom = MainTheme.spacings.double,
),
verticalAlignment = Alignment.CenterVertically,
) {
val color = getDisplayAccountColor(account)
val name = getDisplayAccountName(account)
SideRailAccountIndicator(
accountColor = color,
modifier = Modifier
.fillMaxHeight()
.padding(end = MainTheme.spacings.oneHalf),
)
Column(
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.half),
) {
TextBodyLarge(
text = name,
color = MainTheme.colors.onSurface,
)
if (account is MailDisplayAccount && account.name != account.email) {
TextBodyMedium(
text = account.email,
color = MainTheme.colors.onSurfaceVariant,
)
}
}
}
}
}
@Composable
fun getDisplayCutOutHorizontalInsetPadding(): WindowInsets {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
return WindowInsets.displayCutout.only(if (isRtl) WindowInsetsSides.Right else WindowInsetsSides.Left)
}

View file

@ -0,0 +1,40 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui.setting
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icon
import app.k9mail.core.ui.compose.theme2.MainTheme
@Composable
internal fun SideRailSettingItem(
icon: ImageVector,
label: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier.width(MainTheme.sizes.large),
contentAlignment = Alignment.Center,
) {
Surface(
color = MainTheme.colors.surfaceContainer,
shape = CircleShape,
) {
Icon(
imageVector = icon,
contentDescription = label,
modifier = Modifier
.clickable(onClick = onClick)
.padding(MainTheme.spacings.oneHalf),
)
}
}
}

View file

@ -0,0 +1,49 @@
package net.thunderbird.feature.navigation.drawer.siderail.ui.setting
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.feature.navigation.drawer.dropdown.R
import net.thunderbird.feature.navigation.drawer.dropdown.ui.setting.SettingListItem
@Composable
internal fun SideRailSettingList(
onAccountSelectorClick: () -> Unit,
onManageFoldersClick: () -> Unit,
showAccountSelector: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.padding(vertical = MainTheme.spacings.default)
.windowInsetsPadding(WindowInsets.navigationBars)
.fillMaxWidth(),
) {
SettingListItem(
label = stringResource(R.string.navigation_drawer_dropdown_action_manage_folders),
onClick = onManageFoldersClick,
icon = Icons.Outlined.FolderManaged,
)
SettingListItem(
label = if (showAccountSelector) {
stringResource(R.string.navigation_drawer_dropdown_action_hide_accounts)
} else {
stringResource(R.string.navigation_drawer_dropdown_action_show_accounts)
},
onClick = onAccountSelectorClick,
icon = if (showAccountSelector) {
Icons.Outlined.ChevronLeft
} else {
Icons.Outlined.ChevronRight
},
)
}
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="navigation_drawer_dropdown_action_manage_folders">ማህደሮችን ማስተዳደር</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">ሁሉንም አካውንቶች አሳይ</string>
<string name="navigation_drawer_dropdown_action_show_accounts">አካውንቶችን አሳይ</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">አካውንቶችን ደብቅ</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">ጠቅላላ መልእክት ማስቀመጫ</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+ ዘጠና ዘጠኝ እና ከዛ በላይ</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">الإعدادات</string>
<string name="navigation_drawer_dropdown_action_manage_folders">إدارة المجلدات</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">البريد الوارد</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">إخفاء الحسابات</string>
<string name="navigation_drawer_dropdown_action_show_accounts">إظهار الحسابات</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1k+</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">مزامنة جميع الحسابات</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">قائمة الحسابات</string>
<string name="navigation_drawer_dropdown_unified_account_title">حساب موحد</string>
<string name="navigation_drawer_dropdown_action_add_account">إضافة حساب</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,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Налады</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Кіраванне каталогамі</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Усе атрыманыя</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Сінхранізаваць усе акаўнты</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Паказаць акаўнты</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Схаваць акаўнты</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1000+</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Настройки</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Управление на папки</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">входящи</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Списък с профили</string>
<string name="navigation_drawer_dropdown_unified_account_title">Обединен профил</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Синхронизация на всички профили</string>
<string name="navigation_drawer_dropdown_action_add_account">Добавяне на профил</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Показване на профилите</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Скриване на профилите</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1000+</string>
</resources>

View file

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

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Arventennoù</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Merañ an teuliadoù</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Boest degemer</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Roll ar c\'hontoù</string>
<string name="navigation_drawer_dropdown_unified_account_title">Kont unanet</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Sinkronaat an holl gontoù</string>
<string name="navigation_drawer_dropdown_action_add_account">Ouzhpennañ ur gont</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Diskwel ar c\'hontoù</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Kuzhat ar c\'hontoù</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">&gt;99</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">&gt;1 k</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Postavke</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Upravljajte direktorijumima</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Configuració</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Gestioneu les carpetes</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Bústia d\'entrada unificada</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Sincronitza tots els comptes</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Mostra els comptes</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Amaga els comptes</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1000+</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Llistat de comptes</string>
<string name="navigation_drawer_dropdown_unified_account_title">Compte unificat</string>
<string name="navigation_drawer_dropdown_action_add_account">Afegir compte</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Parametri</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Ghjestione di i cartulari</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Currieru ricevutu</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Sincrunizà tutti i conti</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Affissà i conti</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">&gt; 99</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Piattà i conti</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">&gt; 1000</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Lista di i conti</string>
<string name="navigation_drawer_dropdown_unified_account_title">Contu di cuncolta</string>
<string name="navigation_drawer_dropdown_action_add_account">Aghjunghje un contu</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Nastavení</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Správa složek</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Doručené</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1 tis.+</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Zobrazit účty</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Synchronizovat všechny účty</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Skrýt účty</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Seznam účtů</string>
<string name="navigation_drawer_dropdown_unified_account_title">Jednotný účet</string>
<string name="navigation_drawer_dropdown_action_add_account">Přidat účet</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Gosodiadau</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Rheoli ffolderi</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Blwch Derbyn</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Rhestr cyfrifon</string>
<string name="navigation_drawer_dropdown_unified_account_title">Cyfrif Cyfun</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Cydweddu pob cyfrif</string>
<string name="navigation_drawer_dropdown_action_add_account">Ychwanegu cyfrif</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Dangos cyfrifon</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Cuddio cyfrifon</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1k+</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Indstillinger</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Håndtér mapper</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Fælles indbakke</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Einstellungen</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Ordner verwalten</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Posteingang</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1k+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Konten ausblenden</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Alle Konten synchronisieren</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Konten anzeigen</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Kontenliste</string>
<string name="navigation_drawer_dropdown_action_add_account">Konto hinzufügen</string>
<string name="navigation_drawer_dropdown_unified_account_title">Gemeinsames Konto</string>
</resources>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Ρυθμίσεις</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Διαχείριση φακέλων</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Ενιαία Εισερχόμενα</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1χ+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Συγχρονισμός όλων των λογαριασμών</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Εμφάνιση λογαριασμών</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Απόκρυψη λογαριασμών</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Settings</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Manage folders</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Unified Inbox</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Account list</string>
</resources>

View file

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

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Agordoj</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Administri mesaĝujojn</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Unuigita ricevujo</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Montri kontojn</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Samtempigi ĉiujn kontojn</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Kaŝi kontojn</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Ajustes</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Administrar carpetas</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Recibidos</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">&gt;99</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">&gt;mil</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Sincronizar todas las cuentas</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Mostrar cuentas</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Ocultar cuentas</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Cuentas disponibles</string>
<string name="navigation_drawer_dropdown_unified_account_title">Cuenta unificada</string>
<string name="navigation_drawer_dropdown_action_add_account">Añadir cuenta</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Seadistused</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Halda kaustu</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Sisendkaust</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Sünkroniseeri kõik kasutajakontod</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Näita kasutajakontosid</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Peida kasutajakontod</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1k+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Kasutajakontode loend</string>
<string name="navigation_drawer_dropdown_unified_account_title">Koondkasutajakonto</string>
<string name="navigation_drawer_dropdown_action_add_account">Lisa kasutajakonto</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Ezarpenak</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Kudeatu karpetak</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Sarrerako ontzi bateratua</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Sinkronizatu kontu guztiak</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Erakutsi kontuak</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Ezkutatu kontuak</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1.000+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Kontuen zerrenda</string>
<string name="navigation_drawer_dropdown_unified_account_title">Bateratutako kontua</string>
<string name="navigation_drawer_dropdown_action_add_account">Gehitu kontua</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">تنظیمات</string>
<string name="navigation_drawer_dropdown_action_manage_folders">مدیریت پوشه‌ها</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">صندوق ورودی</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">نهفتن حساب‌ها</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">هم‌گام سازی همهٔ آشنایان</string>
<string name="navigation_drawer_dropdown_action_show_accounts">نمایش حساب‌ها</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">۹۹+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">ه+</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">لیست حساب</string>
<string name="navigation_drawer_dropdown_unified_account_title">حساب یکپارچه</string>
<string name="navigation_drawer_dropdown_action_add_account">افزودن حساب</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Asetukset</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Hallitse kansioita</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Postilaatikko</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Synkronoi kaikki tilit</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Näytä tilit</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Piilota tilit</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1k+</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Tililuettelo</string>
<string name="navigation_drawer_dropdown_unified_account_title">Yhdistetty tili</string>
<string name="navigation_drawer_dropdown_action_add_account">Lisää tili</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Paramètres</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Gérer les dossiers</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Boîte de réception</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Cacher les comptes</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">&gt;99</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">&gt;1 k</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Afficher les comptes</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Synchroniser tous les comptes</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Liste des comptes</string>
<string name="navigation_drawer_dropdown_unified_account_title">Compte unifié</string>
<string name="navigation_drawer_dropdown_action_add_account">Ajouter un compte</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Ynstellingen</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Mappen beheare</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Postfek YN</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1k+</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Alle accounts syngronisearje</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Accounts toane</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Accounts ferstopje</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Accountlist</string>
<string name="navigation_drawer_dropdown_unified_account_title">Kombinearre account</string>
<string name="navigation_drawer_dropdown_action_add_account">Account tafoegje</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="navigation_drawer_dropdown_action_settings">Socruithe</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Bainistigh fillteáin</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Sioncronaigh gach cuntas</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1k+</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Taispeáin cuntais</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Folaigh cuntais</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Bosca isteach</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Liosta cuntas</string>
<string name="navigation_drawer_dropdown_unified_account_title">Cuntas Aontaithe</string>
<string name="navigation_drawer_dropdown_action_add_account">Cuir cuntas leis</string>
</resources>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Roghainnean</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Stiùirich na pasganan</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">An t-oll-bhogsa</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">Còrr is 99</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">Còrr is 1k</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Sioncronaich a h-uile cunntas</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Seall na cunntasan</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Falaich na cunntasan</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Configuración</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Xestionar cartafoles</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Entrada unificada</string>
</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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">सेटिंग</string>
<string name="navigation_drawer_dropdown_action_manage_folders">फोल्डर मैनेज करें</string>
</resources>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Podešenja</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Upravljanje mapama</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Objedinjena dolazna pošta</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Sinkroniziraj sve račune</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Prikaži račune</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Sakrij račune</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1k+</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="navigation_drawer_dropdown_action_settings">Beállítások</string>
<string name="navigation_drawer_dropdown_action_manage_folders">Mappák kezelése</string>
<string name="navigation_drawer_dropdown_unified_inbox_title">Bérkezett üzenetek</string>
<string name="navigation_drawer_dropdown_action_sync_all_accounts">Összes fiók szinkronizálása</string>
<string name="navigation_drawer_dropdown_action_show_accounts">Fiókok megjelenítése</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_99">99+</string>
<string name="navigation_drawer_dropdown_action_hide_accounts">Fiókok elrejtése</string>
<string name="navigation_drawer_dropdown_folder_item_badge_count_greater_than_1_000">1e+</string>
<string name="navigation_drawer_dropdown_avount_view_selection_title">Fióklista</string>
<string name="navigation_drawer_dropdown_unified_account_title">Egyesített fiók</string>
<string name="navigation_drawer_dropdown_action_add_account">Fiók hozzáadása</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more