feat: configurable bottom navigation • part 3 (#1117)

This commit is contained in:
Diego Beraldin 2024-07-12 14:12:56 +02:00 committed by GitHub
parent 29ecfe8e1c
commit 3f99e68764
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 1156 additions and 130 deletions

View File

@ -0,0 +1,84 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.screen.Screen
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.BottomSheetHeader
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInt
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toReadableName
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
class SelectTabNavigationSectionBottomSheet(
private val values: List<TabNavigationSection>,
) : Screen {
@Composable
override fun Content() {
val navigationCoordinator = remember { getNavigationCoordinator() }
val notificationCenter = remember { getNotificationCenter() }
Column(
modifier =
Modifier
.windowInsetsPadding(WindowInsets.navigationBars)
.padding(
top = Spacing.s,
start = Spacing.s,
end = Spacing.s,
bottom = Spacing.m,
),
verticalArrangement = Arrangement.spacedBy(Spacing.s),
) {
BottomSheetHeader(LocalStrings.current.selectTabNavigationTitle)
Column(
modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(Spacing.xxs),
) {
for (value in values) {
Row(
modifier =
Modifier
.padding(
horizontal = Spacing.s,
vertical = Spacing.s,
).fillMaxWidth()
.onClick(
onClick = {
notificationCenter.send(
NotificationCenterEvent.TabNavigationSectionSelected(value.toInt()),
)
navigationCoordinator.hideBottomSheet()
},
),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = value.toReadableName(),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
)
}
}
}
}
}
}

View File

@ -429,4 +429,6 @@ internal open class DefaultStrings : Strings {
override val settingsSubtitleOpenPostWebPageOnImageClick =
"If a post has an URL, open web page on image click"
override val settingsItemAlternateMarkdownRendering = "Enable alternate Markdown rendering"
override val settingsItemConfigureBottomNavigationBar = "Configure navigation bar"
override val selectTabNavigationTitle = "Select a section"
}

View File

@ -427,6 +427,8 @@ interface Strings {
val settingsItemOpenPostWebPageOnImageClick: String
val settingsSubtitleOpenPostWebPageOnImageClick: String
val settingsItemAlternateMarkdownRendering: String
val settingsItemConfigureBottomNavigationBar: String
val selectTabNavigationTitle: String
}
object Locales {

View File

@ -42,6 +42,7 @@ kotlin {
implementation(libs.voyager.screenmodel)
implementation(libs.voyager.koin)
implementation(projects.core.l10n)
implementation(projects.core.persistence)
implementation(projects.core.preferences)
implementation(projects.domain.lemmy.data)

View File

@ -1,5 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.core.navigation.di
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.BottomNavItemsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.DrawerCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.NavigationCoordinator
import org.koin.java.KoinJavaComponent.inject
@ -13,3 +14,8 @@ actual fun getDrawerCoordinator(): DrawerCoordinator {
val res: DrawerCoordinator by inject(DrawerCoordinator::class.java)
return res
}
actual fun getBottomNavItemsRepository(): BottomNavItemsRepository {
val res: BottomNavItemsRepository by inject(BottomNavItemsRepository::class.java)
return res
}

View File

@ -79,8 +79,18 @@ class DefaultBottomNavItemsRepositoryTest {
runTest {
val otherAccountId = 1L
val accountId = 2L
every { keyStore.get("BottomNavItemsRepository.$otherAccountId.items", any<List<String>>()) } returns ITEMS_IDS
every { keyStore.get("BottomNavItemsRepository.$accountId.items", any<List<String>>()) } returns emptyList()
every {
keyStore.get(
"BottomNavItemsRepository.$otherAccountId.items",
any<List<String>>(),
)
} returns ITEMS_IDS
every {
keyStore.get(
"BottomNavItemsRepository.$accountId.items",
any<List<String>>(),
)
} returns emptyList()
val res = sut.get(accountId)
@ -94,8 +104,18 @@ class DefaultBottomNavItemsRepositoryTest {
fun givenDataForOtherUser_whenGetForAnonymousAccount_thenResultAndInteractionsIsAsExpected() =
runTest {
val otherAccountId = 1
every { keyStore.get("BottomNavItemsRepository.$otherAccountId.items", any<List<String>>()) } returns ITEMS_IDS
every { keyStore.get("BottomNavItemsRepository.items", any<List<String>>()) } returns emptyList()
every {
keyStore.get(
"BottomNavItemsRepository.$otherAccountId.items",
any<List<String>>(),
)
} returns ITEMS_IDS
every {
keyStore.get(
"BottomNavItemsRepository.items",
any<List<String>>(),
)
} returns emptyList()
val res = sut.get(null)
@ -105,6 +125,28 @@ class DefaultBottomNavItemsRepositoryTest {
}
}
@Test
fun whenUpdateAnonymousUser_thenInteractionsAreAsExpected() =
runTest {
sut.update(accountId = null, items = ITEMS)
coVerify {
keyStore.save("BottomNavItemsRepository.items", ITEMS_IDS)
}
}
@Test
fun whenUpdateLoggedUser_thenInteractionsAreAsExpected() =
runTest {
val accountId = 1L
sut.update(accountId = accountId, items = ITEMS)
coVerify {
keyStore.save("BottomNavItemsRepository.1.items", ITEMS_IDS)
}
}
companion object {
private val ITEMS_IDS = listOf("0", "1", "3", "2", "4")
private val ITEMS =

View File

@ -11,7 +11,7 @@ internal class DefaultBottomNavItemsRepository(
override suspend fun get(accountId: Long?): List<TabNavigationSection> =
withContext(Dispatchers.IO) {
val key = getKey(accountId)
val itemIds = keyStore.get(key, emptyList())
val itemIds = keyStore.get(key, emptyList()).mapNotNull { it.toIntOrNull() }
val res = itemIds.mapNotNull { it.toTabNavigationSection() }.takeUnless { it.isEmpty() }
res ?: BottomNavItemsRepository.DEFAULT_ITEMS
}
@ -21,7 +21,7 @@ internal class DefaultBottomNavItemsRepository(
items: List<TabNavigationSection>,
) = withContext(Dispatchers.IO) {
val key = getKey(accountId)
val itemIds = items.map { it.toTabNavigationId() }
val itemIds = items.map { it.toInt().toString() }
keyStore.save(key, itemIds)
}
@ -35,24 +35,3 @@ internal class DefaultBottomNavItemsRepository(
append(".items")
}
}
private fun String.toTabNavigationSection(): TabNavigationSection? =
when (this) {
"0" -> TabNavigationSection.Home
"1" -> TabNavigationSection.Explore
"2" -> TabNavigationSection.Inbox
"3" -> TabNavigationSection.Profile
"4" -> TabNavigationSection.Settings
"5" -> TabNavigationSection.Bookmarks
else -> null
}
private fun TabNavigationSection.toTabNavigationId(): String =
when (this) {
TabNavigationSection.Home -> "0"
TabNavigationSection.Explore -> "1"
TabNavigationSection.Inbox -> "2"
TabNavigationSection.Profile -> "3"
TabNavigationSection.Settings -> "4"
TabNavigationSection.Bookmarks -> "5"
}

View File

@ -11,20 +11,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlin.time.Duration
sealed interface TabNavigationSection {
data object Home : TabNavigationSection
data object Explore : TabNavigationSection
data object Profile : TabNavigationSection
data object Inbox : TabNavigationSection
data object Settings : TabNavigationSection
data object Bookmarks : TabNavigationSection
}
sealed interface ComposeEvent {
data class WithUrl(
val url: String,

View File

@ -0,0 +1,54 @@
package com.github.diegoberaldin.raccoonforlemmy.core.navigation
import androidx.compose.runtime.Composable
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings
sealed interface TabNavigationSection {
data object Home : TabNavigationSection
data object Explore : TabNavigationSection
data object Profile : TabNavigationSection
data object Inbox : TabNavigationSection
data object Settings : TabNavigationSection
data object Bookmarks : TabNavigationSection
}
@Composable
fun TabNavigationSection.toReadableName(): String =
when (this) {
TabNavigationSection.Bookmarks -> LocalStrings.current.navigationDrawerTitleBookmarks
TabNavigationSection.Explore -> LocalStrings.current.navigationSearch
TabNavigationSection.Home -> LocalStrings.current.navigationHome
TabNavigationSection.Inbox -> LocalStrings.current.navigationInbox
TabNavigationSection.Profile -> LocalStrings.current.navigationProfile
TabNavigationSection.Settings -> LocalStrings.current.navigationSettings
}
fun Int.toTabNavigationSection(): TabNavigationSection? =
when (this) {
0 -> TabNavigationSection.Home
1 -> TabNavigationSection.Explore
2 -> TabNavigationSection.Inbox
3 -> TabNavigationSection.Profile
4 -> TabNavigationSection.Settings
5 -> TabNavigationSection.Bookmarks
else -> null
}
fun TabNavigationSection.toInt(): Int =
when (this) {
TabNavigationSection.Home -> 0
TabNavigationSection.Explore -> 1
TabNavigationSection.Inbox -> 2
TabNavigationSection.Profile -> 3
TabNavigationSection.Settings -> 4
TabNavigationSection.Bookmarks -> 5
}
fun List<Int>.toTabNavigationSections(): List<TabNavigationSection> = mapNotNull { it.toTabNavigationSection() }
fun List<TabNavigationSection>.toInts(): List<Int> = map { it.toInt() }

View File

@ -1,8 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.core.navigation.di
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.BottomNavItemsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.DrawerCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.NavigationCoordinator
expect fun getNavigationCoordinator(): NavigationCoordinator
expect fun getDrawerCoordinator(): DrawerCoordinator
expect fun getBottomNavItemsRepository(): BottomNavItemsRepository

View File

@ -1,5 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.core.navigation.di
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.BottomNavItemsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.DrawerCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.NavigationCoordinator
import org.koin.core.component.KoinComponent
@ -9,7 +10,10 @@ actual fun getNavigationCoordinator() = CoreNavigationHelper.navigationCoordinat
actual fun getDrawerCoordinator() = CoreNavigationHelper.drawerCoordinator
actual fun getBottomNavItemsRepository() = CoreNavigationHelper.bottomNavItemsRepository
object CoreNavigationHelper : KoinComponent {
val navigationCoordinator: NavigationCoordinator by inject()
val drawerCoordinator: DrawerCoordinator by inject()
val bottomNavItemsRepository: BottomNavItemsRepository by inject()
}

View File

@ -245,4 +245,8 @@ sealed interface NotificationCenterEvent {
data object FavoritesUpdated : NotificationCenterEvent
data object OpenSearchInExplore : NotificationCenterEvent
data class TabNavigationSectionSelected(
val sectionId: Int,
) : NotificationCenterEvent
}

View File

@ -90,6 +90,15 @@ class DefaultSettingsRepositoryTest {
assertEquals(model, value)
}
@Test
fun whenChangeCurrentBottomBarSections_thenValueIsUpdated() =
runTest {
val sectionIds = listOf(0, 1, 2)
sut.changeCurrentBottomBarSections(sectionIds)
val value = sut.currentBottomBarSections.value
assertEquals(sectionIds, value)
}
@Test
fun whenCreateSettings_thenResultIsAsExpected() =
runTest {

View File

@ -13,6 +13,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.preferences.TemporaryKeySto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
@ -77,6 +78,7 @@ internal class DefaultSettingsRepository(
private val db = provider.getDatabase()
override val currentSettings = MutableStateFlow(SettingsModel())
override val currentBottomBarSections = MutableStateFlow<List<Int>>(emptyList())
override suspend fun createSettings(
settings: SettingsModel,
@ -375,7 +377,10 @@ internal class DefaultSettingsRepository(
settings.defaultExploreResultType,
)
keyStore.save(KeyStoreKeys.RANDOM_THEME_COLOR, settings.randomThemeColor)
keyStore.save(KeyStoreKeys.OPEN_POST_WEB_PAGE_ON_IMAGE_CLICK, settings.openPostWebPageOnImageClick)
keyStore.save(
KeyStoreKeys.OPEN_POST_WEB_PAGE_ON_IMAGE_CLICK,
settings.openPostWebPageOnImageClick,
)
} else {
db.settingsQueries.update(
theme = settings.theme?.toLong(),
@ -466,7 +471,11 @@ internal class DefaultSettingsRepository(
}
override fun changeCurrentSettings(settings: SettingsModel) {
currentSettings.value = settings
currentSettings.update { settings }
}
override fun changeCurrentBottomBarSections(sectionIds: List<Int>) {
currentBottomBarSections.update { sectionIds }
}
}

View File

@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.StateFlow
@Stable
interface SettingsRepository {
val currentSettings: StateFlow<SettingsModel>
val currentBottomBarSections: StateFlow<List<Int>>
suspend fun createSettings(
settings: SettingsModel,
@ -21,4 +22,6 @@ interface SettingsRepository {
)
fun changeCurrentSettings(settings: SettingsModel)
fun changeCurrentBottomBarSections(sectionIds: List<Int>)
}

View File

@ -1,3 +1,3 @@
{
"alternateMarkdownRenderingSettingsItemEnabled": true
"alternateMarkdownRenderingSettingsItemEnabled": false
}

View File

@ -40,6 +40,7 @@ kotlin {
implementation(projects.core.utils)
implementation(projects.core.appearance)
implementation(projects.core.persistence)
implementation(projects.core.navigation)
implementation(projects.core.notifications)
implementation(projects.domain.lemmy.repository)
implementation(projects.domain.lemmy.data)
@ -58,8 +59,14 @@ kotlin {
android {
namespace = "com.github.diegoberaldin.raccoonforlemmy.domain.identity"
compileSdk = libs.versions.android.targetSdk.get().toInt()
compileSdk =
libs.versions.android.targetSdk
.get()
.toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
minSdk =
libs.versions.android.minSdk
.get()
.toInt()
}
}

View File

@ -1,6 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.identity.usecase
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.LoginResponse
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.BottomNavItemsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.AccountModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.SettingsModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository
@ -40,6 +41,10 @@ class DefaultLoginUseCaseTest {
private val communitySortRepository = mockk<CommunitySortRepository>(relaxUnitFun = true)
private val communityPreferredLanguageRepository =
mockk<CommunityPreferredLanguageRepository>(relaxUnitFun = true)
private val bottomNavItemsRepository =
mockk<BottomNavItemsRepository>(relaxUnitFun = true) {
coEvery { get(accountId = any()) } returns BottomNavItemsRepository.DEFAULT_ITEMS
}
private val lemmyValueCache = mockk<LemmyValueCache>(relaxUnitFun = true)
private val sut =
DefaultLoginUseCase(
@ -51,6 +56,7 @@ class DefaultLoginUseCaseTest {
siteRepository = siteRepository,
communitySortRepository = communitySortRepository,
communityPreferredLanguageRepository = communityPreferredLanguageRepository,
bottomNavItemsRepository = bottomNavItemsRepository,
lemmyValueCache = lemmyValueCache,
)

View File

@ -1,5 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.identity.usecase
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.BottomNavItemsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.AccountModel
@ -27,6 +28,10 @@ class DefaultLogoutUseCaseTest {
private val notificationCenter = mockk<NotificationCenter>(relaxUnitFun = true)
private val communitySortRepository = mockk<CommunitySortRepository>(relaxUnitFun = true)
private val lemmyValueCache = mockk<LemmyValueCache>(relaxUnitFun = true)
private val bottomNavItemsRepository =
mockk<BottomNavItemsRepository>(relaxUnitFun = true) {
coEvery { get(accountId = any()) } returns BottomNavItemsRepository.DEFAULT_ITEMS
}
private val sut =
DefaultLogoutUseCase(
identityRepository = identityRepository,
@ -34,6 +39,7 @@ class DefaultLogoutUseCaseTest {
settingsRepository = settingsRepository,
notificationCenter = notificationCenter,
communitySortRepository = communitySortRepository,
bottomNavItemsRepository = bottomNavItemsRepository,
lemmyValueCache = lemmyValueCache,
)

View File

@ -1,6 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.identity.usecase
import com.github.diegoberaldin.raccoonforlemmy.core.api.provider.ServiceProvider
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.BottomNavItemsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.AccountModel
@ -32,6 +33,10 @@ class DefaultSwitchAccountUseCaseTest {
private val communityPreferredLanguageRepository =
mockk<CommunityPreferredLanguageRepository>(relaxUnitFun = true)
private val lemmyValueCache = mockk<LemmyValueCache>(relaxUnitFun = true)
private val bottomNavItemsRepository =
mockk<BottomNavItemsRepository>(relaxUnitFun = true) {
coEvery { get(accountId = any()) } returns BottomNavItemsRepository.DEFAULT_ITEMS
}
private val sut =
DefaultSwitchAccountUseCase(
identityRepository = identityRepository,
@ -41,6 +46,7 @@ class DefaultSwitchAccountUseCaseTest {
notificationCenter = notificationCenter,
communitySortRepository = communitySortRepository,
communityPreferredLanguageRepository = communityPreferredLanguageRepository,
bottomNavItemsRepository = bottomNavItemsRepository,
lemmyValueCache = lemmyValueCache,
)

View File

@ -47,6 +47,7 @@ val coreIdentityModule =
siteRepository = get(),
communitySortRepository = get(),
communityPreferredLanguageRepository = get(),
bottomNavItemsRepository = get(),
lemmyValueCache = get(),
)
}
@ -57,6 +58,7 @@ val coreIdentityModule =
notificationCenter = get(),
settingsRepository = get(),
communitySortRepository = get(),
bottomNavItemsRepository = get(),
lemmyValueCache = get(),
)
}
@ -69,6 +71,7 @@ val coreIdentityModule =
notificationCenter = get(),
communitySortRepository = get(),
communityPreferredLanguageRepository = get(),
bottomNavItemsRepository = get(),
lemmyValueCache = get(),
)
}

View File

@ -1,6 +1,8 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.identity.usecase
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.VoteFormat
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.BottomNavItemsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInts
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.AccountModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.CommunityPreferredLanguageRepository
@ -21,6 +23,7 @@ internal class DefaultLoginUseCase(
private val siteRepository: SiteRepository,
private val communitySortRepository: CommunitySortRepository,
private val communityPreferredLanguageRepository: CommunityPreferredLanguageRepository,
private val bottomNavItemsRepository: BottomNavItemsRepository,
private val lemmyValueCache: LemmyValueCache,
) : LoginUseCase {
override suspend operator fun invoke(
@ -106,6 +109,9 @@ internal class DefaultLoginUseCase(
val newSettings = settingsRepository.getSettings(accountId)
settingsRepository.changeCurrentSettings(newSettings)
val bottomBarSections = bottomNavItemsRepository.get(accountId)
settingsRepository.changeCurrentBottomBarSections(bottomBarSections.toInts())
}
}
}

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.identity.usecase
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.BottomNavItemsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInts
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository
@ -14,6 +16,7 @@ internal class DefaultLogoutUseCase(
private val notificationCenter: NotificationCenter,
private val settingsRepository: SettingsRepository,
private val communitySortRepository: CommunitySortRepository,
private val bottomNavItemsRepository: BottomNavItemsRepository,
private val lemmyValueCache: LemmyValueCache,
) : LogoutUseCase {
override suspend operator fun invoke() {
@ -31,5 +34,8 @@ internal class DefaultLogoutUseCase(
}
val anonSettings = settingsRepository.getSettings(null)
settingsRepository.changeCurrentSettings(anonSettings)
val bottomBarSections = bottomNavItemsRepository.get(null)
settingsRepository.changeCurrentBottomBarSections(bottomBarSections.toInts())
}
}

View File

@ -1,6 +1,8 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.identity.usecase
import com.github.diegoberaldin.raccoonforlemmy.core.api.provider.ServiceProvider
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.BottomNavItemsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInts
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.AccountModel
@ -19,6 +21,7 @@ internal class DefaultSwitchAccountUseCase(
private val settingsRepository: SettingsRepository,
private val communitySortRepository: CommunitySortRepository,
private val communityPreferredLanguageRepository: CommunityPreferredLanguageRepository,
private val bottomNavItemsRepository: BottomNavItemsRepository,
private val lemmyValueCache: LemmyValueCache,
) : SwitchAccountUseCase {
override suspend fun invoke(account: AccountModel) {
@ -44,5 +47,8 @@ internal class DefaultSwitchAccountUseCase(
val newSettings = settingsRepository.getSettings(accountId)
settingsRepository.changeCurrentSettings(newSettings)
val bottomBarSections = bottomNavItemsRepository.get(accountId)
settingsRepository.changeCurrentBottomBarSections(bottomBarSections.toInts())
}
}

View File

@ -8,6 +8,8 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInt
import com.github.diegoberaldin.raccoonforlemmy.unit.postlist.PostListScreen
object HomeTab : Tab {
@ -18,7 +20,7 @@ object HomeTab : Tab {
val title = LocalStrings.current.navigationHome
return TabOptions(
index = 0u,
index = TabNavigationSection.Home.toInt().toUShort(),
title = title,
icon = icon,
)

View File

@ -8,6 +8,8 @@ import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInt
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxScreen
object InboxTab : Tab {
@ -17,7 +19,7 @@ object InboxTab : Tab {
val icon = rememberVectorPainter(Icons.Default.Inbox)
val title = LocalStrings.current.navigationInbox
return TabOptions(
index = 3u,
index = TabNavigationSection.Inbox.toInt().toUShort(),
title = title,
icon = icon,
)

View File

@ -31,6 +31,7 @@ val profileTabModule =
}
factory<ProfileSideMenuMviModel> {
ProfileSideMenuViewModel(
settingsRepository = get(),
lemmyValueCache = get(),
)
}

View File

@ -27,6 +27,7 @@ internal fun ProfileMenuContent(
modifier: Modifier = Modifier,
isModerator: Boolean = false,
canCreateCommunity: Boolean = false,
isBookmarksVisible: Boolean = true,
) {
val notificationCenter = remember { getNotificationCenter() }
@ -42,14 +43,16 @@ internal fun ProfileMenuContent(
notificationCenter.send(NotificationCenterEvent.ProfileSideMenuAction.ManageSubscriptions)
},
)
SettingsRow(
title = LocalStrings.current.navigationDrawerTitleBookmarks,
icon = Icons.Default.Bookmark,
onTap =
rememberCallback {
notificationCenter.send(NotificationCenterEvent.ProfileSideMenuAction.Bookmarks)
},
)
if (isBookmarksVisible) {
SettingsRow(
title = LocalStrings.current.navigationDrawerTitleBookmarks,
icon = Icons.Default.Bookmark,
onTap =
rememberCallback {
notificationCenter.send(NotificationCenterEvent.ProfileSideMenuAction.Bookmarks)
},
)
}
SettingsRow(
title = LocalStrings.current.navigationDrawerTitleDrafts,
icon = Icons.Default.Drafts,

View File

@ -11,6 +11,7 @@ interface ProfileSideMenuMviModel :
data class State(
val isModerator: Boolean = false,
val canCreateCommunity: Boolean = false,
val isBookmarksVisible: Boolean = true,
)
sealed interface Effect

View File

@ -70,6 +70,7 @@ class ProfileSideMenuScreen : Screen {
),
isModerator = uiState.isModerator,
canCreateCommunity = uiState.canCreateCommunity,
isBookmarksVisible = uiState.isBookmarksVisible,
)
}
}

View File

@ -2,6 +2,9 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.profile.menu
import cafe.adriel.voyager.core.model.screenModelScope
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toTabNavigationSections
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.LemmyValueCache
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
@ -9,6 +12,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class ProfileSideMenuViewModel(
private val settingsRepository: SettingsRepository,
private val lemmyValueCache: LemmyValueCache,
) : DefaultMviModel<ProfileSideMenuMviModel.Intent, ProfileSideMenuMviModel.State, ProfileSideMenuMviModel.Effect>(
ProfileSideMenuMviModel.State(),
@ -39,6 +43,16 @@ class ProfileSideMenuViewModel(
)
}
}.launchIn(this)
settingsRepository.currentBottomBarSections
.onEach { sectionIds ->
val isBookmarksInBottomBar =
sectionIds
.toTabNavigationSections()
.contains(TabNavigationSection.Bookmarks)
updateState {
it.copy(isBookmarksVisible = !isBookmarksInBottomBar)
}
}.launchIn(this)
}
}
}

View File

@ -8,6 +8,8 @@ import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInt
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.main.ProfileMainScreen
object ProfileTab : Tab {
@ -16,7 +18,7 @@ object ProfileTab : Tab {
val icon = rememberVectorPainter(Icons.Default.AccountCircle)
val title = LocalStrings.current.navigationProfile
return TabOptions(
index = 2u,
index = TabNavigationSection.Profile.toInt().toUShort(),
title = title,
icon = icon,
)

View File

@ -8,6 +8,8 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInt
import com.github.diegoberaldin.raccoonforlemmy.unit.explore.ExploreScreen
object ExploreTab : Tab {
@ -16,7 +18,7 @@ object ExploreTab : Tab {
val icon = rememberVectorPainter(Icons.Default.Explore)
val title = LocalStrings.current.navigationSearch
return TabOptions(
index = 1u,
index = TabNavigationSection.Explore.toInt().toUShort(),
title = title,
icon = icon,
)

View File

@ -67,6 +67,7 @@ kotlin {
implementation(projects.unit.choosecolor)
implementation(projects.unit.choosefont)
implementation(projects.unit.configureswipeactions)
implementation(projects.unit.configurenavbar)
implementation(projects.unit.configurecontentview)
implementation(projects.unit.filteredcontents)
implementation(projects.unit.medialist)
@ -82,8 +83,14 @@ kotlin {
android {
namespace = "com.github.diegoberaldin.raccoonforlemmy.feature.settings"
compileSdk = libs.versions.android.targetSdk.get().toInt()
compileSdk =
libs.versions.android.targetSdk
.get()
.toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
minSdk =
libs.versions.android.minSdk
.get()
.toInt()
}
}

View File

@ -8,6 +8,8 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInt
import com.github.diegoberaldin.raccoonforlemmy.feature.settings.main.SettingsScreen
object SettingsTab : Tab {
@ -18,7 +20,7 @@ object SettingsTab : Tab {
val title = LocalStrings.current.navigationSettings
return TabOptions(
index = 4u,
index = TabNavigationSection.Settings.toInt().toUShort(),
title = title,
icon = icon,
)

View File

@ -69,6 +69,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.utils.datetime.getPrettyDur
import com.github.diegoberaldin.raccoonforlemmy.core.utils.fs.getFileSystemManager
import com.github.diegoberaldin.raccoonforlemmy.core.utils.toLocalDp
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toReadableName
import com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar.ConfigureNavBarScreen
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -159,6 +160,7 @@ class AdvancedSettingsScreen : Screen {
Modifier
.padding(
top = padding.calculateTopPadding(),
bottom = padding.calculateBottomPadding(),
).nestedScroll(scrollBehavior.nestedScrollConnection),
) {
Column(
@ -495,6 +497,14 @@ class AdvancedSettingsScreen : Screen {
title = LocalStrings.current.settingsTitleExperimental,
icon = Icons.Default.Science,
)
SettingsRow(
title = LocalStrings.current.settingsItemConfigureBottomNavigationBar,
disclosureIndicator = true,
onTap =
rememberCallback {
navigationCoordinator.pushScreen(ConfigureNavBarScreen())
},
)
if (uiState.alternateMarkdownRenderingItemVisible) {
// alternate Markdown rendering
SettingsSwitchRow(

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -154,8 +155,8 @@ class SettingsScreen : Screen {
Modifier
.padding(
top = padding.calculateTopPadding(),
bottom = padding.calculateBottomPadding(),
).nestedScroll(scrollBehavior.nestedScrollConnection),
).navigationBarsPadding()
.nestedScroll(scrollBehavior.nestedScrollConnection),
) {
Column(
modifier = Modifier.fillMaxSize().verticalScroll(scrollState),

View File

@ -61,6 +61,7 @@ include(":unit:choosefont")
include(":unit:communitydetail")
include(":unit:communityinfo")
include(":unit:configurecontentview")
include(":unit:configurenavbar")
include(":unit:configureswipeactions")
include(":unit:createcomment")
include(":unit:createpost")

View File

@ -76,6 +76,7 @@ kotlin {
implementation(projects.unit.communitydetail)
implementation(projects.unit.communityinfo)
implementation(projects.unit.configurecontentview)
implementation(projects.unit.configurenavbar)
implementation(projects.unit.configureswipeactions)
implementation(projects.unit.createcomment)
implementation(projects.unit.createpost)

View File

@ -35,6 +35,7 @@ import com.github.diegoberaldin.raccoonforlemmy.unit.chat.di.chatModule
import com.github.diegoberaldin.raccoonforlemmy.unit.communitydetail.di.communityDetailModule
import com.github.diegoberaldin.raccoonforlemmy.unit.communityinfo.di.communityInfoModule
import com.github.diegoberaldin.raccoonforlemmy.unit.configurecontentview.di.configureContentViewModule
import com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar.di.configureNavBarModule
import com.github.diegoberaldin.raccoonforlemmy.unit.configureswipeactions.di.configureSwipeActionsModule
import com.github.diegoberaldin.raccoonforlemmy.unit.createcomment.di.createCommentModule
import com.github.diegoberaldin.raccoonforlemmy.unit.createpost.di.createPostModule
@ -119,5 +120,6 @@ val sharedHelperModule =
moderateWithReasonModule,
acknowledgementsModule,
mediaListModule,
configureNavBarModule,
)
}

View File

@ -57,8 +57,10 @@ import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.ProvideString
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.ComposeEvent
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.DrawerEvent
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.SideMenuEvents
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getBottomNavItemsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getDrawerCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInts
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.getAccountRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.getSettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.di.getAppConfigStore
@ -108,9 +110,12 @@ fun App(onLoadingFinished: () -> Unit = {}) {
val scope = rememberCoroutineScope()
val subscriptionsCache = remember { getSubscriptionsCache() }
val appConfigStore = remember { getAppConfigStore() }
val bottomNavItemsRepository = remember { getBottomNavItemsRepository() }
LaunchedEffect(Unit) {
val accountId = accountRepository.getActive()?.id
val lastActiveAccount = accountRepository.getActive()
val lastInstance = lastActiveAccount?.instance?.takeIf { it.isNotEmpty() }
val accountId = lastActiveAccount?.id
val currentSettings = settingsRepository.getSettings(accountId)
val seedColor =
if (currentSettings.randomThemeColor) {
@ -122,8 +127,8 @@ fun App(onLoadingFinished: () -> Unit = {}) {
currentSettings.customSeedColor
}
settingsRepository.changeCurrentSettings(currentSettings.copy(customSeedColor = seedColor))
val lastActiveAccount = accountRepository.getActive()
val lastInstance = lastActiveAccount?.instance?.takeIf { it.isNotEmpty() }
val bottomBarSections = bottomNavItemsRepository.get(accountId)
settingsRepository.changeCurrentBottomBarSections(bottomBarSections.toInts())
if (lastInstance != null) {
apiConfigurationRepository.changeInstance(lastInstance)
}
@ -132,6 +137,7 @@ fun App(onLoadingFinished: () -> Unit = {}) {
appConfigStore.initialize()
hasBeenInitialized = true
launch {
delay(50)
onLoadingFinished()

View File

@ -43,10 +43,10 @@ import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getDrawerCoor
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallbackArgs
import com.github.diegoberaldin.raccoonforlemmy.feature.home.ui.HomeTab
import com.github.diegoberaldin.raccoonforlemmy.feature.settings.main.SettingsScreen
import com.github.diegoberaldin.raccoonforlemmy.navigation.TabNavigationItem
import com.github.diegoberaldin.raccoonforlemmy.navigation.toTab
import com.github.diegoberaldin.raccoonforlemmy.unit.manageaccounts.ManageAccountsScreen
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.drop
@ -277,7 +277,8 @@ internal object MainScreen : Screen {
),
tonalElevation = 0.dp,
) {
for (section in uiState.bottomBarSections) {
// it must be done so (indexed), otherwise section gets remembered in tap callbacks
uiState.bottomBarSections.forEachIndexed { idx, section ->
TabNavigationItem(
section = section,
withText = titleVisible,
@ -287,15 +288,17 @@ internal object MainScreen : Screen {
} else {
null
},
onClick =
rememberCallbackArgs { tab ->
tabNavigator.current = tab
navigationCoordinator.setCurrentSection(section)
},
onLongPress =
rememberCallbackArgs { tab ->
handleOnLongPress(tab, section)
},
onClick = {
val section = uiState.bottomBarSections[idx]
val tab = section.toTab()
tabNavigator.current = tab
navigationCoordinator.setCurrentSection(section)
},
onLongPress = {
val section = uiState.bottomBarSections[idx]
val tab = section.toTab()
handleOnLongPress(tab, section)
},
)
}
}

View File

@ -2,8 +2,7 @@ package com.github.diegoberaldin.raccoonforlemmy
import cafe.adriel.voyager.core.model.screenModelScope
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.BottomNavItemsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toTabNavigationSections
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.inbox.InboxCoordinator
@ -21,8 +20,6 @@ class MainViewModel(
private val identityRepository: IdentityRepository,
private val settingRepository: SettingsRepository,
private val userRepository: UserRepository,
private val accountRepository: AccountRepository,
private val bottomNavItemsRepository: BottomNavItemsRepository,
private val notificationChecker: InboxNotificationChecker,
private val lemmyValueCache: LemmyValueCache,
) : DefaultMviModel<MainMviModel.Intent, MainMviModel.UiState, MainMviModel.Effect>(
@ -53,17 +50,20 @@ class MainViewModel(
notificationChecker.stop()
}
}.launchIn(this)
settingRepository.currentSettings
.onEach {
updateCustomProfileIcon()
}.launchIn(this)
settingRepository.currentBottomBarSections
.onEach { sectionIds ->
val sections = sectionIds.toTabNavigationSections()
updateState { it.copy(bottomBarSections = sections) }
}.launchIn(this)
identityRepository.isLogged
.onEach { isLogged ->
updateState { it.copy(isLogged = isLogged ?: false) }
updateCustomProfileIcon()
refreshBottomNavigationItems()
}.launchIn(this)
}
}
@ -106,12 +106,4 @@ class MainViewModel(
emitEffect(MainMviModel.Effect.ReadAllInboxSuccess)
}
}
private suspend fun refreshBottomNavigationItems() {
val accountId = accountRepository.getActive()?.id
val sections = bottomNavItemsRepository.get(accountId)
updateState {
it.copy(bottomBarSections = sections)
}
}
}

View File

@ -14,8 +14,6 @@ internal val internalSharedModule =
identityRepository = get(),
settingRepository = get(),
userRepository = get(),
accountRepository = get(),
bottomNavItemsRepository = get(),
notificationChecker = get(),
lemmyValueCache = get(),
)

View File

@ -8,6 +8,8 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInt
import com.github.diegoberaldin.raccoonforlemmy.unit.filteredcontents.FilteredContentsScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.filteredcontents.FilteredContentsType
import com.github.diegoberaldin.raccoonforlemmy.unit.filteredcontents.toInt
@ -20,7 +22,7 @@ object BookmarksTab : Tab {
val title = LocalStrings.current.navigationDrawerTitleBookmarks
return TabOptions(
index = 5u,
index = TabNavigationSection.Bookmarks.toInt().toUShort(),
title = title,
icon = icon,
)

View File

@ -42,8 +42,8 @@ internal fun RowScope.TabNavigationItem(
section: TabNavigationSection,
withText: Boolean = true,
customIconUrl: String? = null,
onClick: ((Tab) -> Unit)? = null,
onLongPress: ((Tab) -> Unit)? = null,
onClick: (() -> Unit)? = null,
onLongPress: (() -> Unit)? = null,
) {
val navigationCoordinator = remember { getNavigationCoordinator() }
val unread by navigationCoordinator.inboxUnread.collectAsState()
@ -51,10 +51,6 @@ internal fun RowScope.TabNavigationItem(
val interactionSource = remember { MutableInteractionSource() }
val tab = section.toTab()
fun handleClick() {
onClick?.invoke(tab)
}
val pointerInputModifier =
Modifier.pointerInput(Unit) {
detectTapGestures(
@ -65,16 +61,18 @@ internal fun RowScope.TabNavigationItem(
interactionSource.emit(PressInteraction.Release(press))
},
onTap = {
handleClick()
onClick?.invoke()
},
onLongPress = {
onLongPress?.invoke(tab)
onLongPress?.invoke()
},
)
}
NavigationBarItem(
onClick = ::handleClick,
onClick = {
onClick?.invoke()
},
selected = section == currentSection,
interactionSource = interactionSource,
icon = {
@ -136,7 +134,7 @@ internal fun RowScope.TabNavigationItem(
)
}
private fun TabNavigationSection.toTab(): Tab =
internal fun TabNavigationSection.toTab(): Tab =
when (this) {
TabNavigationSection.Explore -> ExploreTab
TabNavigationSection.Profile -> ProfileTab

View File

@ -36,6 +36,7 @@ import com.github.diegoberaldin.raccoonforlemmy.unit.chat.di.chatModule
import com.github.diegoberaldin.raccoonforlemmy.unit.communitydetail.di.communityDetailModule
import com.github.diegoberaldin.raccoonforlemmy.unit.communityinfo.di.communityInfoModule
import com.github.diegoberaldin.raccoonforlemmy.unit.configurecontentview.di.configureContentViewModule
import com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar.di.configureNavBarModule
import com.github.diegoberaldin.raccoonforlemmy.unit.configureswipeactions.di.configureSwipeActionsModule
import com.github.diegoberaldin.raccoonforlemmy.unit.createcomment.di.createCommentModule
import com.github.diegoberaldin.raccoonforlemmy.unit.createpost.di.createPostModule
@ -122,6 +123,7 @@ fun initKoin() {
moderateWithReasonModule,
acknowledgementsModule,
mediaListModule,
configureNavBarModule,
)
}

View File

@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
@ -110,17 +111,18 @@ class AccountSettingsScreen : Screen {
var confirmBackWithUnsavedChangesDialog by remember { mutableStateOf(false) }
LaunchedEffect(model) {
model.effects.onEach { evt ->
when (evt) {
AccountSettingsMviModel.Effect.Failure -> {
snackbarHostState.showSnackbar(errorMessage)
}
model.effects
.onEach { evt ->
when (evt) {
AccountSettingsMviModel.Effect.Failure -> {
snackbarHostState.showSnackbar(errorMessage)
}
AccountSettingsMviModel.Effect.Success -> {
snackbarHostState.showSnackbar(successMessage)
AccountSettingsMviModel.Effect.Success -> {
snackbarHostState.showSnackbar(successMessage)
}
}
}
}.launchIn(this)
}.launchIn(this)
}
DisposableEffect(key) {
@ -214,8 +216,7 @@ class AccountSettingsScreen : Screen {
Modifier
.padding(
top = padding.calculateTopPadding(),
bottom = Spacing.m,
)
).navigationBarsPadding()
.then(
if (settings.hideNavigationBarWhileScrolling) {
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)

View File

@ -0,0 +1,78 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.android.library)
alias(libs.plugins.jetbrains.compose)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.detekt)
}
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
kotlin {
applyDefaultHierarchyTemplate()
androidTarget {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
}
}
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64(),
).forEach {
it.binaries.framework {
baseName = "unit.configurenavbar"
isStatic = true
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(libs.koin.core)
implementation(libs.reorderable)
implementation(libs.voyager.navigator)
implementation(libs.voyager.screenmodel)
implementation(libs.voyager.koin)
implementation(projects.core.appearance)
implementation(projects.core.architecture)
implementation(projects.core.commonui.components)
implementation(projects.core.commonui.modals)
implementation(projects.core.l10n)
implementation(projects.core.navigation)
implementation(projects.core.notifications)
implementation(projects.core.persistence)
implementation(projects.core.utils)
implementation(projects.domain.identity)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
}
}
android {
namespace = "com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar"
compileSdk =
libs.versions.android.targetSdk
.get()
.toInt()
defaultConfig {
minSdk =
libs.versions.android.minSdk
.get()
.toInt()
}
}

View File

@ -0,0 +1,36 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar
import androidx.compose.runtime.Stable
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection
@Stable
interface ConfigureNavBarMviModel :
MviModel<ConfigureNavBarMviModel.Intent, ConfigureNavBarMviModel.UiState, ConfigureNavBarMviModel.Effect>,
ScreenModel {
sealed interface Intent {
data object Reset : Intent
data object HapticFeedback : Intent
data class SwapItems(
val from: Int,
val to: Int,
) : Intent
data class Delete(
val section: TabNavigationSection,
) : Intent
data object Save : Intent
}
data class UiState(
val sections: List<TabNavigationSection> = emptyList(),
val availableSections: List<TabNavigationSection> = emptyList(),
val hasUnsavedChanges: Boolean = false,
)
sealed interface Effect
}

View File

@ -0,0 +1,279 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SelectTabNavigationSectionBottomSheet
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInt
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toReadableName
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.getSettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback
import com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar.composable.ConfigureAddAction
import com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar.composable.ConfigureNavBarItem
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
class ConfigureNavBarScreen : Screen {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
override fun Content() {
val model = getScreenModel<ConfigureNavBarMviModel>()
val uiState by model.uiState.collectAsState()
val navigationCoordinator = remember { getNavigationCoordinator() }
val topAppBarState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState)
val settingsRepository = remember { getSettingsRepository() }
val settings by settingsRepository.currentSettings.collectAsState()
val lazyListState = rememberLazyListState()
val reorderableLazyColumnState =
rememberReorderableLazyListState(lazyListState) { from, to ->
model.reduce(
ConfigureNavBarMviModel.Intent.SwapItems(
from = from.index - 1,
to = to.index - 1,
),
)
}
var confirmBackWithUnsavedChangesDialog by remember { mutableStateOf(false) }
DisposableEffect(key) {
navigationCoordinator.setCanGoBackCallback {
if (uiState.hasUnsavedChanges) {
confirmBackWithUnsavedChangesDialog = true
return@setCanGoBackCallback false
}
true
}
onDispose {
navigationCoordinator.setCanGoBackCallback(null)
}
}
Scaffold(
modifier =
Modifier
.background(MaterialTheme.colorScheme.background)
.padding(Spacing.xs),
topBar = {
TopAppBar(
scrollBehavior = scrollBehavior,
title = {
Text(
modifier = Modifier.padding(horizontal = Spacing.s),
text = LocalStrings.current.settingsItemConfigureBottomNavigationBar,
style = MaterialTheme.typography.titleMedium,
)
},
navigationIcon = {
if (navigationCoordinator.canPop.value) {
Image(
modifier =
Modifier.onClick(
onClick = {
if (uiState.hasUnsavedChanges) {
confirmBackWithUnsavedChangesDialog = true
} else {
navigationCoordinator.popScreen()
}
},
),
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
}
},
actions = {
TextButton(
contentPadding =
PaddingValues(
horizontal = Spacing.xs,
vertical = Spacing.xxs,
),
onClick = {
model.reduce(ConfigureNavBarMviModel.Intent.Reset)
},
) {
Text(
text = LocalStrings.current.buttonReset,
style = MaterialTheme.typography.labelSmall,
)
}
},
)
},
) { padding ->
Column(
modifier =
Modifier
.padding(
top = padding.calculateTopPadding(),
).navigationBarsPadding()
.then(
if (settings.hideNavigationBarWhileScrolling) {
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
Modifier
},
),
) {
LazyColumn(
modifier = Modifier.weight(1f).fillMaxWidth(),
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
if (uiState.sections.isEmpty()) {
item {
Text(
modifier = Modifier.fillMaxWidth().padding(top = Spacing.xs),
textAlign = TextAlign.Center,
text = LocalStrings.current.messageEmptyList,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
)
}
} else {
item {
// workaround for https://github.com/Calvin-LL/Reorderable/issues/4
ReorderableItem(
state = reorderableLazyColumnState,
key = "dummy",
enabled = false,
modifier = Modifier.fillMaxWidth().height(Dp.Hairline),
) {}
}
}
items(
items = uiState.sections,
key = { it.toInt().toString() },
) { section ->
ReorderableItem(
state = reorderableLazyColumnState,
key = section.toInt().toString(),
) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
Surface(
shadowElevation = elevation,
) {
ConfigureNavBarItem(
reorderableScope = this,
title = section.toReadableName(),
onDragStarted =
rememberCallback(model) {
model.reduce(ConfigureNavBarMviModel.Intent.HapticFeedback)
},
onDelete =
rememberCallback(model) {
model.reduce(ConfigureNavBarMviModel.Intent.Delete(section))
},
)
}
}
}
if (uiState.availableSections.isNotEmpty() && uiState.sections.size < 5) {
item {
ConfigureAddAction(
onAdd =
rememberCallback {
val available = model.uiState.value.availableSections
val sheet =
SelectTabNavigationSectionBottomSheet(values = available)
navigationCoordinator.showBottomSheet(sheet)
},
)
}
}
}
Box(
modifier = Modifier.fillMaxWidth().padding(vertical = Spacing.m),
contentAlignment = Alignment.Center,
) {
Button(
enabled = uiState.hasUnsavedChanges,
onClick = {
model.reduce(ConfigureNavBarMviModel.Intent.Save)
},
) {
Text(text = LocalStrings.current.actionSave)
}
}
}
}
if (confirmBackWithUnsavedChangesDialog) {
AlertDialog(
onDismissRequest = {
confirmBackWithUnsavedChangesDialog = false
},
dismissButton = {
Button(
onClick = {
confirmBackWithUnsavedChangesDialog = false
},
) {
Text(text = LocalStrings.current.buttonNoStay)
}
},
confirmButton = {
Button(
onClick = {
confirmBackWithUnsavedChangesDialog = false
navigationCoordinator.popScreen()
},
) {
Text(text = LocalStrings.current.buttonYesQuit)
}
},
text = {
Text(text = LocalStrings.current.messageUnsavedChanges)
},
)
}
}
}

View File

@ -0,0 +1,150 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar
import cafe.adriel.voyager.core.model.screenModelScope
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.BottomNavItemsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toInts
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toTabNavigationSection
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
internal class ConfigureNavBarViewModel(
private val accountRepository: AccountRepository,
private val identityRepository: IdentityRepository,
private val bottomNavItemsRepository: BottomNavItemsRepository,
private val settingsRepository: SettingsRepository,
private val hapticFeedback: HapticFeedback,
private val notificationCenter: NotificationCenter,
) : DefaultMviModel<ConfigureNavBarMviModel.Intent, ConfigureNavBarMviModel.UiState, ConfigureNavBarMviModel.Effect>(
initialState = ConfigureNavBarMviModel.UiState(),
),
ConfigureNavBarMviModel {
init {
screenModelScope.launch {
notificationCenter
.subscribe(NotificationCenterEvent.TabNavigationSectionSelected::class)
.onEach { evt ->
evt.sectionId.toTabNavigationSection()?.also { section ->
handleAdd(section)
}
}.launchIn(this)
refresh()
}
}
override fun reduce(intent: ConfigureNavBarMviModel.Intent) {
when (intent) {
ConfigureNavBarMviModel.Intent.HapticFeedback -> hapticFeedback.vibrate()
ConfigureNavBarMviModel.Intent.Reset -> handleReset()
is ConfigureNavBarMviModel.Intent.Delete -> handleDelete(intent.section)
is ConfigureNavBarMviModel.Intent.SwapItems -> handleSwap(intent.from, intent.to)
ConfigureNavBarMviModel.Intent.Save -> save()
}
}
private suspend fun refresh() {
val accountId = accountRepository.getActive()?.id
val currentSections = bottomNavItemsRepository.get(accountId)
updateState {
it.copy(
sections = currentSections,
availableSections = getAvailableSections(currentSections),
)
}
}
private fun getAvailableSections(excludeSections: List<TabNavigationSection> = emptyList()): List<TabNavigationSection> {
val availableSections =
buildList {
this += TabNavigationSection.Home
this += TabNavigationSection.Explore
this += TabNavigationSection.Inbox
this += TabNavigationSection.Profile
this += TabNavigationSection.Settings
if (identityRepository.isLogged.value == true) {
this += TabNavigationSection.Bookmarks
}
}
return availableSections.filter { section -> section !in excludeSections }
}
private fun handleReset() {
screenModelScope.launch {
val oldSections = uiState.value.sections
val newSections = BottomNavItemsRepository.DEFAULT_ITEMS
updateState {
it.copy(
sections = newSections,
availableSections = getAvailableSections(newSections),
hasUnsavedChanges = newSections != oldSections,
)
}
}
}
private fun handleAdd(section: TabNavigationSection) {
val newSections = uiState.value.sections + section
screenModelScope.launch {
updateState {
it.copy(
sections = newSections,
availableSections = getAvailableSections(newSections),
hasUnsavedChanges = true,
)
}
}
}
private fun handleDelete(section: TabNavigationSection) {
val newSections = uiState.value.sections - section
screenModelScope.launch {
updateState {
it.copy(
sections = newSections,
availableSections = getAvailableSections(newSections),
hasUnsavedChanges = true,
)
}
}
}
private fun handleSwap(
from: Int,
to: Int,
) {
val newSections =
uiState.value.sections.toMutableList().apply {
val element = removeAt(from)
add(to, element)
}
screenModelScope.launch {
updateState {
it.copy(
sections = newSections,
hasUnsavedChanges = true,
)
}
}
}
private fun save() {
screenModelScope.launch {
val currentSections = uiState.value.sections
val accountId = accountRepository.getActive()?.id
bottomNavItemsRepository.update(accountId, currentSections)
updateState {
it.copy(hasUnsavedChanges = false)
}
settingsRepository.changeCurrentBottomBarSections(currentSections.toInts())
}
}
}

View File

@ -0,0 +1,48 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar.composable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.IconSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.ancillaryTextAlpha
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
@Composable
internal fun ConfigureAddAction(onAdd: () -> Unit) {
val ancillaryColor = MaterialTheme.colorScheme.onBackground.copy(alpha = ancillaryTextAlpha)
Row(
modifier =
Modifier
.padding(
horizontal = Spacing.s,
vertical = Spacing.xs,
).onClick(onClick = onAdd),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
modifier = Modifier.size(IconSize.m),
imageVector = Icons.Outlined.Add,
contentDescription = null,
tint = ancillaryColor,
)
Spacer(modifier = Modifier.width(Spacing.xs))
Text(
text = LocalStrings.current.buttonAdd,
style = MaterialTheme.typography.labelMedium,
)
}
}

View File

@ -0,0 +1,115 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar.composable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.unit.DpOffset
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.IconSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.ancillaryTextAlpha
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomDropDown
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.toLocalDp
import sh.calvin.reorderable.ReorderableCollectionItemScope
@Composable
internal fun ConfigureNavBarItem(
modifier: Modifier = Modifier,
reorderableScope: ReorderableCollectionItemScope,
title: String,
onDragStarted: (() -> Unit)? = null,
onDelete: (() -> Unit)? = null,
) {
var optionsOffset by remember { mutableStateOf(Offset.Zero) }
var optionsMenuOpen by remember { mutableStateOf(false) }
val fullColor = MaterialTheme.colorScheme.onBackground
val ancillaryColor = MaterialTheme.colorScheme.onBackground.copy(alpha = ancillaryTextAlpha)
Row(
modifier =
modifier
.fillMaxWidth()
.padding(
horizontal = Spacing.m,
vertical = Spacing.s,
),
horizontalArrangement = Arrangement.spacedBy(Spacing.s),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = title,
color = fullColor,
)
Spacer(modifier = Modifier.weight(1f))
Box {
Icon(
modifier =
Modifier
.size(IconSize.m)
.padding(Spacing.xs)
.then(
with(reorderableScope) {
Modifier.draggableHandle(
onDragStarted = {
onDragStarted?.invoke()
},
)
},
).onGloballyPositioned {
optionsOffset = it.positionInParent()
}.onClick(
onClick = {
optionsMenuOpen = true
},
),
imageVector = Icons.Default.MoreVert,
contentDescription = null,
tint = ancillaryColor,
)
CustomDropDown(
expanded = optionsMenuOpen,
onDismiss = {
optionsMenuOpen = false
},
offset =
DpOffset(
x = optionsOffset.x.toLocalDp(),
y = optionsOffset.y.toLocalDp(),
),
) {
DropdownMenuItem(
text = {
Text(LocalStrings.current.commentActionDelete)
},
onClick = {
optionsMenuOpen = false
onDelete?.invoke()
},
)
}
}
}
}

View File

@ -0,0 +1,19 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar.di
import com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar.ConfigureNavBarMviModel
import com.github.diegoberaldin.raccoonforlemmy.unit.configurenavbar.ConfigureNavBarViewModel
import org.koin.dsl.module
val configureNavBarModule =
module {
single<ConfigureNavBarMviModel> {
ConfigureNavBarViewModel(
accountRepository = get(),
identityRepository = get(),
bottomNavItemsRepository = get(),
settingsRepository = get(),
hapticFeedback = get(),
notificationCenter = get(),
)
}
}

View File

@ -205,22 +205,24 @@ object ModalDrawerContent : Tab {
)
}
}
item {
DrawerShortcut(
title = LocalStrings.current.navigationSettings,
icon = Icons.Default.Settings,
onSelected =
rememberCallback(coordinator) {
scope.launch {
focusManager.clearFocus()
navigationCoordinator.popUntilRoot()
coordinator.toggleDrawer()
delay(50)
if (uiState.isSettingsVisible) {
item {
DrawerShortcut(
title = LocalStrings.current.navigationSettings,
icon = Icons.Default.Settings,
onSelected =
rememberCallback(coordinator) {
scope.launch {
focusManager.clearFocus()
navigationCoordinator.popUntilRoot()
coordinator.toggleDrawer()
delay(50)
coordinator.sendEvent(DrawerEvent.OpenSettings)
}
},
)
coordinator.sendEvent(DrawerEvent.OpenSettings)
}
},
)
}
}
}

View File

@ -36,6 +36,7 @@ interface ModalDrawerMviModel :
val searchText: String = "",
val isFiltering: Boolean = false,
val enableToggleFavorite: Boolean = false,
val isSettingsVisible: Boolean = true,
)
sealed interface Effect

View File

@ -4,6 +4,8 @@ import cafe.adriel.voyager.core.model.screenModelScope
import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.CommunityPaginationManager
import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.CommunityPaginationSpecification
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.toTabNavigationSections
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.FavoriteCommunityModel
@ -98,6 +100,14 @@ class ModalDrawerViewModel(
)
}
}.launchIn(this)
settingsRepository.currentBottomBarSections
.onEach { sectionIds ->
val isSettingsInBottomBar =
sectionIds.toTabNavigationSections().contains(TabNavigationSection.Settings)
updateState {
it.copy(isSettingsVisible = !isSettingsInBottomBar)
}
}.launchIn(this)
uiState
.map { it.searchText }