Merge pull request #2489 from vitorpamplona/claude/configurable-bottom-nav-rFdQw
Add customizable bottom navigation bar with drag-to-reorder UI
This commit is contained in:
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.model
|
|||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.DefaultBottomBarItems
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItem
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@@ -40,6 +42,7 @@ data class UiSettings(
|
|||||||
val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED,
|
val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED,
|
||||||
val gallerySet: ProfileGalleryType = ProfileGalleryType.CLASSIC,
|
val gallerySet: ProfileGalleryType = ProfileGalleryType.CLASSIC,
|
||||||
val automaticallyProposeAiImprovements: BooleanType = BooleanType.ALWAYS,
|
val automaticallyProposeAiImprovements: BooleanType = BooleanType.ALWAYS,
|
||||||
|
val bottomBarItems: List<NavBarItem> = DefaultBottomBarItems,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class ThemeType(
|
enum class ThemeType(
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.DefaultBottomBarItems
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItem
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@@ -40,6 +42,7 @@ class UiSettingsFlow(
|
|||||||
val featureSet: MutableStateFlow<FeatureSetType> = MutableStateFlow(FeatureSetType.SIMPLIFIED),
|
val featureSet: MutableStateFlow<FeatureSetType> = MutableStateFlow(FeatureSetType.SIMPLIFIED),
|
||||||
val gallerySet: MutableStateFlow<ProfileGalleryType> = MutableStateFlow(ProfileGalleryType.CLASSIC),
|
val gallerySet: MutableStateFlow<ProfileGalleryType> = MutableStateFlow(ProfileGalleryType.CLASSIC),
|
||||||
val automaticallyProposeAiImprovements: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
val automaticallyProposeAiImprovements: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
||||||
|
val bottomBarItems: MutableStateFlow<List<NavBarItem>> = MutableStateFlow(DefaultBottomBarItems),
|
||||||
) {
|
) {
|
||||||
val listOfFlows: List<Flow<Any?>> =
|
val listOfFlows: List<Flow<Any?>> =
|
||||||
listOf<Flow<Any?>>(
|
listOf<Flow<Any?>>(
|
||||||
@@ -56,9 +59,11 @@ class UiSettingsFlow(
|
|||||||
featureSet,
|
featureSet,
|
||||||
gallerySet,
|
gallerySet,
|
||||||
automaticallyProposeAiImprovements,
|
automaticallyProposeAiImprovements,
|
||||||
|
bottomBarItems,
|
||||||
)
|
)
|
||||||
|
|
||||||
// emits at every change in any of the propertyes.
|
// emits at every change in any of the propertyes.
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val propertyWatchFlow: Flow<UiSettings> =
|
val propertyWatchFlow: Flow<UiSettings> =
|
||||||
combine<Any?, UiSettings>(listOfFlows) { flows: Array<Any?> ->
|
combine<Any?, UiSettings>(listOfFlows) { flows: Array<Any?> ->
|
||||||
UiSettings(
|
UiSettings(
|
||||||
@@ -75,6 +80,7 @@ class UiSettingsFlow(
|
|||||||
flows[10] as FeatureSetType,
|
flows[10] as FeatureSetType,
|
||||||
flows[11] as ProfileGalleryType,
|
flows[11] as ProfileGalleryType,
|
||||||
flows[12] as BooleanType,
|
flows[12] as BooleanType,
|
||||||
|
flows[13] as List<NavBarItem>,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +99,7 @@ class UiSettingsFlow(
|
|||||||
featureSet.value,
|
featureSet.value,
|
||||||
gallerySet.value,
|
gallerySet.value,
|
||||||
automaticallyProposeAiImprovements.value,
|
automaticallyProposeAiImprovements.value,
|
||||||
|
bottomBarItems.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun update(torSettings: UiSettings): Boolean {
|
fun update(torSettings: UiSettings): Boolean {
|
||||||
@@ -150,6 +157,10 @@ class UiSettingsFlow(
|
|||||||
automaticallyProposeAiImprovements.tryEmit(torSettings.automaticallyProposeAiImprovements)
|
automaticallyProposeAiImprovements.tryEmit(torSettings.automaticallyProposeAiImprovements)
|
||||||
any = true
|
any = true
|
||||||
}
|
}
|
||||||
|
if (bottomBarItems.value != torSettings.bottomBarItems) {
|
||||||
|
bottomBarItems.tryEmit(torSettings.bottomBarItems)
|
||||||
|
any = true
|
||||||
|
}
|
||||||
|
|
||||||
return any
|
return any
|
||||||
}
|
}
|
||||||
@@ -182,6 +193,7 @@ class UiSettingsFlow(
|
|||||||
MutableStateFlow(uiSettings.featureSet),
|
MutableStateFlow(uiSettings.featureSet),
|
||||||
MutableStateFlow(uiSettings.gallerySet),
|
MutableStateFlow(uiSettings.gallerySet),
|
||||||
MutableStateFlow(uiSettings.automaticallyProposeAiImprovements),
|
MutableStateFlow(uiSettings.automaticallyProposeAiImprovements),
|
||||||
|
MutableStateFlow(uiSettings.bottomBarItems),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
@@ -38,6 +38,8 @@ import com.vitorpamplona.amethyst.model.ProfileGalleryType
|
|||||||
import com.vitorpamplona.amethyst.model.ThemeType
|
import com.vitorpamplona.amethyst.model.ThemeType
|
||||||
import com.vitorpamplona.amethyst.model.UiSettings
|
import com.vitorpamplona.amethyst.model.UiSettings
|
||||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.DefaultBottomBarItems
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItem
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -104,6 +106,7 @@ class UiSharedPreferences(
|
|||||||
val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set")
|
val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set")
|
||||||
val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_set")
|
val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_set")
|
||||||
val UI_PROPOSE_AI_IMPROVEMENTS = stringPreferencesKey("ui.propose_ai_improvements")
|
val UI_PROPOSE_AI_IMPROVEMENTS = stringPreferencesKey("ui.propose_ai_improvements")
|
||||||
|
val UI_BOTTOM_BAR_ITEMS = stringPreferencesKey("ui.bottom_bar_items")
|
||||||
|
|
||||||
suspend fun uiPreferences(context: Context): UiSettings? =
|
suspend fun uiPreferences(context: Context): UiSettings? =
|
||||||
try {
|
try {
|
||||||
@@ -124,6 +127,7 @@ class UiSharedPreferences(
|
|||||||
featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED,
|
featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED,
|
||||||
gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC,
|
gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC,
|
||||||
automaticallyProposeAiImprovements = preferences[UI_PROPOSE_AI_IMPROVEMENTS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS,
|
automaticallyProposeAiImprovements = preferences[UI_PROPOSE_AI_IMPROVEMENTS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS,
|
||||||
|
bottomBarItems = preferences[UI_BOTTOM_BAR_ITEMS]?.let { decodeBottomBarItems(it) } ?: DefaultBottomBarItems,
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
@@ -161,6 +165,7 @@ class UiSharedPreferences(
|
|||||||
preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name
|
preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name
|
||||||
preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name
|
preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name
|
||||||
preferences[UI_PROPOSE_AI_IMPROVEMENTS] = sharedSettings.automaticallyProposeAiImprovements.name
|
preferences[UI_PROPOSE_AI_IMPROVEMENTS] = sharedSettings.automaticallyProposeAiImprovements.name
|
||||||
|
preferences[UI_BOTTOM_BAR_ITEMS] = sharedSettings.bottomBarItems.joinToString(",") { it.name }
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
@@ -168,5 +173,18 @@ class UiSharedPreferences(
|
|||||||
Log.e("SharedPreferences") { "Error saving DataStore preferences: ${e.message}" }
|
Log.e("SharedPreferences") { "Error saving DataStore preferences: ${e.message}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun decodeBottomBarItems(raw: String): List<NavBarItem> {
|
||||||
|
if (raw.isEmpty()) return emptyList()
|
||||||
|
return raw
|
||||||
|
.split(",")
|
||||||
|
.mapNotNull { name ->
|
||||||
|
try {
|
||||||
|
NavBarItem.valueOf(name)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.vanish.RequestToVani
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.vanish.VanishEventsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.vanish.VanishEventsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AllSettingsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AllSettingsScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.BottomBarSettingsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.CallSettingsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.CallSettingsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen
|
||||||
@@ -303,6 +304,7 @@ fun BuildNavigation(
|
|||||||
composableFromEnd<Route.Settings> { SettingsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.Settings> { SettingsScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.ReactionsSettings> { ReactionsSettingsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.ReactionsSettings> { ReactionsSettingsScreen(accountViewModel, nav) }
|
||||||
|
composableFromEnd<Route.BottomBarSettings> { BottomBarSettingsScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.CallSettings> { CallSettingsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.CallSettings> { CallSettingsScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.ImportFollowsSelectUser> { ImportFollowListSelectUserScreen(accountViewModel, nav) }
|
composableFromEnd<Route.ImportFollowsSelectUser> { ImportFollowListSelectUserScreen(accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.ImportFollowsPickFollows> {
|
composableFromEndArgs<Route.ImportFollowsPickFollows> {
|
||||||
|
|||||||
+53
-13
@@ -25,9 +25,11 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material3.BottomAppBarDefaults.windowInsets
|
import androidx.compose.material3.BottomAppBarDefaults.windowInsets
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -37,6 +39,7 @@ import androidx.compose.material3.NavigationBar
|
|||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -49,6 +52,8 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size0dp
|
import com.vitorpamplona.amethyst.ui.theme.Size0dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size10Modifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size23dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppBottomBar(
|
fun AppBottomBar(
|
||||||
@@ -56,18 +61,33 @@ fun AppBottomBar(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (Route) -> Unit,
|
nav: (Route) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val items by accountViewModel.settings.uiSettingsFlow.bottomBarItems
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
Spacer(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.windowInsetsPadding(windowInsets)
|
||||||
|
.consumeWindowInsets(windowInsets),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
val isKeyboardState by keyboardAsState()
|
val isKeyboardState by keyboardAsState()
|
||||||
if (isKeyboardState == KeyboardState.Closed) {
|
if (isKeyboardState == KeyboardState.Closed) {
|
||||||
RenderBottomMenu(selectedRoute, accountViewModel, nav)
|
RenderBottomMenu(items, selectedRoute, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderBottomMenu(
|
private fun RenderBottomMenu(
|
||||||
|
items: List<NavBarItem>,
|
||||||
selectedRoute: Route?,
|
selectedRoute: Route?,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (Route) -> Unit,
|
nav: (Route) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val defs = remember(items) { items.mapNotNull(NavBarCatalog::get) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -84,8 +104,9 @@ private fun RenderBottomMenu(
|
|||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
tonalElevation = Size0dp,
|
tonalElevation = Size0dp,
|
||||||
) {
|
) {
|
||||||
bottomNavigationItems.forEach { item ->
|
defs.forEach { def ->
|
||||||
HasNewItemsIcon(item.route == selectedRoute, item, accountViewModel, nav)
|
val destination = remember(def, accountViewModel) { def.resolveRoute(accountViewModel) }
|
||||||
|
HasNewItemsIcon(destination == selectedRoute, def, destination, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +115,8 @@ private fun RenderBottomMenu(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun RowScope.HasNewItemsIcon(
|
private fun RowScope.HasNewItemsIcon(
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
bottomNav: BottomBarRoute,
|
def: NavBarItemDef,
|
||||||
|
destination: Route,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (Route) -> Unit,
|
nav: (Route) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -103,30 +125,48 @@ private fun RowScope.HasNewItemsIcon(
|
|||||||
icon = {
|
icon = {
|
||||||
NotifiableIcon(
|
NotifiableIcon(
|
||||||
selected,
|
selected,
|
||||||
bottomNav,
|
def,
|
||||||
|
destination,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onClick = { nav(bottomNav.route) },
|
onClick = { nav(destination) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NotifiableIcon(
|
private fun NotifiableIcon(
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
route: BottomBarRoute,
|
def: NavBarItemDef,
|
||||||
|
destination: Route,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
Box(route.notifSize) {
|
Box(Modifier.size(Size23dp)) {
|
||||||
|
val tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified
|
||||||
|
val iconSizeModifier = Modifier.size(Size20dp)
|
||||||
|
val description = stringRes(def.labelRes)
|
||||||
|
when (val icon = def.icon) {
|
||||||
|
is NavBarIcon.Drawable -> {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterRes(resourceId = route.icon, 0),
|
painter = painterRes(resourceId = icon.resId, icon.reference),
|
||||||
contentDescription = stringRes(route.contentDescriptor),
|
contentDescription = description,
|
||||||
modifier = route.iconSize,
|
modifier = iconSizeModifier,
|
||||||
tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified,
|
tint = tint,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AddNotifIconIfNeeded(route.route, accountViewModel, Modifier.align(Alignment.TopEnd))
|
is NavBarIcon.Vector -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon.vector,
|
||||||
|
contentDescription = description,
|
||||||
|
modifier = iconSizeModifier,
|
||||||
|
tint = tint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNotifIconIfNeeded(destination, accountViewModel, Modifier.align(Alignment.TopEnd))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Vitor Pamplona
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to use,
|
|
||||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
||||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
package com.vitorpamplona.amethyst.ui.navigation.bottombars
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import com.vitorpamplona.amethyst.R
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size23dp
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
|
|
||||||
class BottomBarRoute(
|
|
||||||
val route: Route,
|
|
||||||
val icon: Int,
|
|
||||||
val contentDescriptor: Int,
|
|
||||||
val notifSize: Modifier = Modifier.size(Size23dp),
|
|
||||||
val iconSize: Modifier = Modifier.size(Size20dp),
|
|
||||||
)
|
|
||||||
|
|
||||||
val bottomNavigationItems =
|
|
||||||
persistentListOf(
|
|
||||||
BottomBarRoute(Route.Home, R.drawable.ic_home, R.string.route_home, Modifier.size(Size25dp), Modifier.size(Size24dp)),
|
|
||||||
BottomBarRoute(Route.Message, R.drawable.ic_dm, R.string.route_messages),
|
|
||||||
BottomBarRoute(Route.Video, R.drawable.ic_video, R.string.route_video),
|
|
||||||
BottomBarRoute(Route.Discover, R.drawable.ic_sensors, R.string.route_discover),
|
|
||||||
BottomBarRoute(Route.Notification(), R.drawable.ic_notifications, R.string.route_notifications),
|
|
||||||
)
|
|
||||||
+334
@@ -0,0 +1,334 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.navigation.bottombars
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.Article
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.Chat
|
||||||
|
import androidx.compose.material.icons.filled.AccountCircle
|
||||||
|
import androidx.compose.material.icons.outlined.AccountBalanceWallet
|
||||||
|
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||||
|
import androidx.compose.material.icons.outlined.Drafts
|
||||||
|
import androidx.compose.material.icons.outlined.EmojiEmotions
|
||||||
|
import androidx.compose.material.icons.outlined.Groups
|
||||||
|
import androidx.compose.material.icons.outlined.Language
|
||||||
|
import androidx.compose.material.icons.outlined.MilitaryTech
|
||||||
|
import androidx.compose.material.icons.outlined.Photo
|
||||||
|
import androidx.compose.material.icons.outlined.PlayCircle
|
||||||
|
import androidx.compose.material.icons.outlined.Sensors
|
||||||
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
import androidx.compose.material.icons.outlined.SmartDisplay
|
||||||
|
import androidx.compose.material.icons.outlined.Storefront
|
||||||
|
import androidx.compose.material.icons.outlined.Tag
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stable identifiers for every drawer destination that the user can pin to the bottom bar.
|
||||||
|
* Order in this enum has no semantic meaning — the user picks a subset and an order at runtime.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
enum class NavBarItem {
|
||||||
|
HOME,
|
||||||
|
MESSAGES,
|
||||||
|
VIDEO,
|
||||||
|
DISCOVER,
|
||||||
|
NOTIFICATIONS,
|
||||||
|
PROFILE,
|
||||||
|
MY_LISTS,
|
||||||
|
BOOKMARKS,
|
||||||
|
WEB_BOOKMARKS,
|
||||||
|
DRAFTS,
|
||||||
|
INTEREST_SETS,
|
||||||
|
EMOJI_PACKS,
|
||||||
|
WALLET,
|
||||||
|
COMMUNITIES,
|
||||||
|
ARTICLES,
|
||||||
|
PICTURES,
|
||||||
|
SHORTS,
|
||||||
|
PUBLIC_CHATS,
|
||||||
|
FOLLOW_PACKS,
|
||||||
|
LIVE_STREAMS,
|
||||||
|
LONGS,
|
||||||
|
POLLS,
|
||||||
|
BADGES,
|
||||||
|
PRODUCTS,
|
||||||
|
EMOJI_SETS,
|
||||||
|
SETTINGS,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class NavBarIcon {
|
||||||
|
data class Drawable(
|
||||||
|
@DrawableRes val resId: Int,
|
||||||
|
val reference: Int = 0,
|
||||||
|
) : NavBarIcon()
|
||||||
|
|
||||||
|
data class Vector(
|
||||||
|
val vector: ImageVector,
|
||||||
|
) : NavBarIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NavBarItemDef(
|
||||||
|
val id: NavBarItem,
|
||||||
|
val labelRes: Int,
|
||||||
|
val icon: NavBarIcon,
|
||||||
|
val resolveRoute: (AccountViewModel) -> Route,
|
||||||
|
)
|
||||||
|
|
||||||
|
val NavBarCatalog: Map<NavBarItem, NavBarItemDef> =
|
||||||
|
linkedMapOf(
|
||||||
|
NavBarItem.HOME to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.HOME,
|
||||||
|
labelRes = R.string.route_home,
|
||||||
|
icon = NavBarIcon.Drawable(R.drawable.ic_home),
|
||||||
|
resolveRoute = { Route.Home },
|
||||||
|
),
|
||||||
|
NavBarItem.MESSAGES to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.MESSAGES,
|
||||||
|
labelRes = R.string.route_messages,
|
||||||
|
icon = NavBarIcon.Drawable(R.drawable.ic_dm),
|
||||||
|
resolveRoute = { Route.Message },
|
||||||
|
),
|
||||||
|
NavBarItem.VIDEO to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.VIDEO,
|
||||||
|
labelRes = R.string.route_video,
|
||||||
|
icon = NavBarIcon.Drawable(R.drawable.ic_video),
|
||||||
|
resolveRoute = { Route.Video },
|
||||||
|
),
|
||||||
|
NavBarItem.DISCOVER to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.DISCOVER,
|
||||||
|
labelRes = R.string.route_discover,
|
||||||
|
icon = NavBarIcon.Drawable(R.drawable.ic_sensors),
|
||||||
|
resolveRoute = { Route.Discover },
|
||||||
|
),
|
||||||
|
NavBarItem.NOTIFICATIONS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.NOTIFICATIONS,
|
||||||
|
labelRes = R.string.route_notifications,
|
||||||
|
icon = NavBarIcon.Drawable(R.drawable.ic_notifications),
|
||||||
|
resolveRoute = { Route.Notification() },
|
||||||
|
),
|
||||||
|
NavBarItem.PROFILE to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.PROFILE,
|
||||||
|
labelRes = R.string.profile,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Default.AccountCircle),
|
||||||
|
resolveRoute = { Route.Profile(it.userProfile().pubkeyHex) },
|
||||||
|
),
|
||||||
|
NavBarItem.MY_LISTS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.MY_LISTS,
|
||||||
|
labelRes = R.string.my_lists,
|
||||||
|
icon = NavBarIcon.Vector(Icons.AutoMirrored.Filled.FormatListBulleted),
|
||||||
|
resolveRoute = { Route.Lists },
|
||||||
|
),
|
||||||
|
NavBarItem.BOOKMARKS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.BOOKMARKS,
|
||||||
|
labelRes = R.string.bookmarks,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.CollectionsBookmark),
|
||||||
|
resolveRoute = { Route.BookmarkGroups },
|
||||||
|
),
|
||||||
|
NavBarItem.WEB_BOOKMARKS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.WEB_BOOKMARKS,
|
||||||
|
labelRes = R.string.web_bookmarks,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.Language),
|
||||||
|
resolveRoute = { Route.WebBookmarks },
|
||||||
|
),
|
||||||
|
NavBarItem.DRAFTS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.DRAFTS,
|
||||||
|
labelRes = R.string.drafts,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.Drafts),
|
||||||
|
resolveRoute = { Route.Drafts },
|
||||||
|
),
|
||||||
|
NavBarItem.INTEREST_SETS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.INTEREST_SETS,
|
||||||
|
labelRes = R.string.interest_sets_title,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.Tag),
|
||||||
|
resolveRoute = { Route.InterestSets },
|
||||||
|
),
|
||||||
|
NavBarItem.EMOJI_PACKS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.EMOJI_PACKS,
|
||||||
|
labelRes = R.string.manage_emoji_packs,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.EmojiEmotions),
|
||||||
|
resolveRoute = { Route.EmojiPacks },
|
||||||
|
),
|
||||||
|
NavBarItem.WALLET to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.WALLET,
|
||||||
|
labelRes = R.string.wallet,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.AccountBalanceWallet),
|
||||||
|
resolveRoute = { Route.Wallet },
|
||||||
|
),
|
||||||
|
NavBarItem.COMMUNITIES to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.COMMUNITIES,
|
||||||
|
labelRes = R.string.communities,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.Groups),
|
||||||
|
resolveRoute = { Route.Communities },
|
||||||
|
),
|
||||||
|
NavBarItem.ARTICLES to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.ARTICLES,
|
||||||
|
labelRes = R.string.discover_reads,
|
||||||
|
icon = NavBarIcon.Vector(Icons.AutoMirrored.Outlined.Article),
|
||||||
|
resolveRoute = { Route.Articles },
|
||||||
|
),
|
||||||
|
NavBarItem.PICTURES to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.PICTURES,
|
||||||
|
labelRes = R.string.pictures,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.Photo),
|
||||||
|
resolveRoute = { Route.Pictures },
|
||||||
|
),
|
||||||
|
NavBarItem.SHORTS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.SHORTS,
|
||||||
|
labelRes = R.string.shorts,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.PlayCircle),
|
||||||
|
resolveRoute = { Route.Shorts },
|
||||||
|
),
|
||||||
|
NavBarItem.PUBLIC_CHATS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.PUBLIC_CHATS,
|
||||||
|
labelRes = R.string.public_chats,
|
||||||
|
icon = NavBarIcon.Vector(Icons.AutoMirrored.Outlined.Chat),
|
||||||
|
resolveRoute = { Route.PublicChats },
|
||||||
|
),
|
||||||
|
NavBarItem.FOLLOW_PACKS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.FOLLOW_PACKS,
|
||||||
|
labelRes = R.string.follow_packs,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.CollectionsBookmark),
|
||||||
|
resolveRoute = { Route.FollowPacks },
|
||||||
|
),
|
||||||
|
NavBarItem.LIVE_STREAMS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.LIVE_STREAMS,
|
||||||
|
labelRes = R.string.live_streams,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.Sensors),
|
||||||
|
resolveRoute = { Route.LiveStreams },
|
||||||
|
),
|
||||||
|
NavBarItem.LONGS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.LONGS,
|
||||||
|
labelRes = R.string.longs,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.SmartDisplay),
|
||||||
|
resolveRoute = { Route.Longs },
|
||||||
|
),
|
||||||
|
NavBarItem.POLLS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.POLLS,
|
||||||
|
labelRes = R.string.polls,
|
||||||
|
icon = NavBarIcon.Drawable(R.drawable.ic_poll, reference = 1),
|
||||||
|
resolveRoute = { Route.Polls },
|
||||||
|
),
|
||||||
|
NavBarItem.BADGES to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.BADGES,
|
||||||
|
labelRes = R.string.badges,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.MilitaryTech),
|
||||||
|
resolveRoute = { Route.Badges },
|
||||||
|
),
|
||||||
|
NavBarItem.PRODUCTS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.PRODUCTS,
|
||||||
|
labelRes = R.string.discover_marketplace,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.Storefront),
|
||||||
|
resolveRoute = { Route.Products },
|
||||||
|
),
|
||||||
|
NavBarItem.EMOJI_SETS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.EMOJI_SETS,
|
||||||
|
labelRes = R.string.emoji_sets,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.EmojiEmotions),
|
||||||
|
resolveRoute = { Route.BrowseEmojiSets },
|
||||||
|
),
|
||||||
|
NavBarItem.SETTINGS to
|
||||||
|
NavBarItemDef(
|
||||||
|
id = NavBarItem.SETTINGS,
|
||||||
|
labelRes = R.string.settings,
|
||||||
|
icon = NavBarIcon.Vector(Icons.Outlined.Settings),
|
||||||
|
resolveRoute = { Route.AllSettings },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val DefaultBottomBarItems: List<NavBarItem> =
|
||||||
|
listOf(
|
||||||
|
NavBarItem.HOME,
|
||||||
|
NavBarItem.MESSAGES,
|
||||||
|
NavBarItem.VIDEO,
|
||||||
|
NavBarItem.DISCOVER,
|
||||||
|
NavBarItem.NOTIFICATIONS,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ordered membership lists for each drawer section. The drawer renders these by looking up
|
||||||
|
// each id in NavBarCatalog, so adding a new screen only requires editing the catalog + the
|
||||||
|
// matching section list below — not two separate files.
|
||||||
|
val DrawerNavigateItems: List<NavBarItem> =
|
||||||
|
listOf(
|
||||||
|
NavBarItem.HOME,
|
||||||
|
NavBarItem.MESSAGES,
|
||||||
|
NavBarItem.VIDEO,
|
||||||
|
NavBarItem.DISCOVER,
|
||||||
|
NavBarItem.NOTIFICATIONS,
|
||||||
|
)
|
||||||
|
|
||||||
|
val DrawerYouItems: List<NavBarItem> =
|
||||||
|
listOf(
|
||||||
|
NavBarItem.PROFILE,
|
||||||
|
NavBarItem.MY_LISTS,
|
||||||
|
NavBarItem.BOOKMARKS,
|
||||||
|
NavBarItem.WEB_BOOKMARKS,
|
||||||
|
NavBarItem.DRAFTS,
|
||||||
|
NavBarItem.INTEREST_SETS,
|
||||||
|
NavBarItem.EMOJI_PACKS,
|
||||||
|
NavBarItem.WALLET,
|
||||||
|
)
|
||||||
|
|
||||||
|
val DrawerFeedsItems: List<NavBarItem> =
|
||||||
|
listOf(
|
||||||
|
NavBarItem.COMMUNITIES,
|
||||||
|
NavBarItem.ARTICLES,
|
||||||
|
NavBarItem.PICTURES,
|
||||||
|
NavBarItem.SHORTS,
|
||||||
|
NavBarItem.PUBLIC_CHATS,
|
||||||
|
NavBarItem.FOLLOW_PACKS,
|
||||||
|
NavBarItem.LIVE_STREAMS,
|
||||||
|
NavBarItem.LONGS,
|
||||||
|
NavBarItem.POLLS,
|
||||||
|
NavBarItem.BADGES,
|
||||||
|
NavBarItem.PRODUCTS,
|
||||||
|
NavBarItem.EMOJI_SETS,
|
||||||
|
)
|
||||||
+89
-186
@@ -50,30 +50,17 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.Send
|
import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Article
|
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Chat
|
|
||||||
import androidx.compose.material.icons.filled.AccountCircle
|
import androidx.compose.material.icons.filled.AccountCircle
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.outlined.AccountBalanceWallet
|
|
||||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||||
import androidx.compose.material.icons.outlined.Drafts
|
import androidx.compose.material.icons.outlined.Drafts
|
||||||
import androidx.compose.material.icons.outlined.EmojiEmotions
|
|
||||||
import androidx.compose.material.icons.outlined.ExpandLess
|
import androidx.compose.material.icons.outlined.ExpandLess
|
||||||
import androidx.compose.material.icons.outlined.ExpandMore
|
import androidx.compose.material.icons.outlined.ExpandMore
|
||||||
import androidx.compose.material.icons.outlined.GroupAdd
|
import androidx.compose.material.icons.outlined.GroupAdd
|
||||||
import androidx.compose.material.icons.outlined.Groups
|
|
||||||
import androidx.compose.material.icons.outlined.Language
|
|
||||||
import androidx.compose.material.icons.outlined.MilitaryTech
|
|
||||||
import androidx.compose.material.icons.outlined.Photo
|
import androidx.compose.material.icons.outlined.Photo
|
||||||
import androidx.compose.material.icons.outlined.PlayCircle
|
|
||||||
import androidx.compose.material.icons.outlined.Sensors
|
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
|
||||||
import androidx.compose.material.icons.outlined.SettingsInputAntenna
|
import androidx.compose.material.icons.outlined.SettingsInputAntenna
|
||||||
import androidx.compose.material.icons.outlined.SmartDisplay
|
import androidx.compose.material.icons.outlined.SmartDisplay
|
||||||
import androidx.compose.material.icons.outlined.Storefront
|
|
||||||
import androidx.compose.material.icons.outlined.Tag
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -122,6 +109,13 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse
|
|||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.DrawerFeedsItems
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.DrawerNavigateItems
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.DrawerYouItems
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarIcon
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItem
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItemDef
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
@@ -549,172 +543,9 @@ fun ListContent(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
CollapsibleSection(title = R.string.drawer_section_you) {
|
CatalogSection(R.string.drawer_section_navigate, DrawerNavigateItems, accountViewModel, nav)
|
||||||
NavigationRow(
|
CatalogSection(R.string.drawer_section_you, DrawerYouItems, accountViewModel, nav)
|
||||||
title = R.string.profile,
|
CatalogSection(R.string.drawer_section_feeds, DrawerFeedsItems, accountViewModel, nav)
|
||||||
icon = Icons.Default.AccountCircle,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
nav = nav,
|
|
||||||
computeRoute = {
|
|
||||||
Route.Profile(accountViewModel.userProfile().pubkeyHex)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.my_lists,
|
|
||||||
icon = Icons.AutoMirrored.Filled.FormatListBulleted,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.Lists,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.bookmarks,
|
|
||||||
icon = Icons.Outlined.CollectionsBookmark,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.BookmarkGroups,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.web_bookmarks,
|
|
||||||
icon = Icons.Outlined.Language,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.WebBookmarks,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.drafts,
|
|
||||||
icon = Icons.Outlined.Drafts,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.Drafts,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.interest_sets_title,
|
|
||||||
icon = Icons.Outlined.Tag,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.InterestSets,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.manage_emoji_packs,
|
|
||||||
icon = Icons.Outlined.EmojiEmotions,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.EmojiPacks,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.wallet,
|
|
||||||
icon = Icons.Outlined.AccountBalanceWallet,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.Wallet,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
CollapsibleSection(title = R.string.drawer_section_feeds) {
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.communities,
|
|
||||||
icon = Icons.Outlined.Groups,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.Communities,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.discover_reads,
|
|
||||||
icon = Icons.AutoMirrored.Outlined.Article,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.Articles,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.pictures,
|
|
||||||
icon = Icons.Outlined.Photo,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.Pictures,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.shorts,
|
|
||||||
icon = Icons.Outlined.PlayCircle,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.Shorts,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.public_chats,
|
|
||||||
icon = Icons.AutoMirrored.Outlined.Chat,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.PublicChats,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.follow_packs,
|
|
||||||
icon = Icons.Outlined.CollectionsBookmark,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.FollowPacks,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.live_streams,
|
|
||||||
icon = Icons.Outlined.Sensors,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.LiveStreams,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.longs,
|
|
||||||
icon = Icons.Outlined.SmartDisplay,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.Longs,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.polls,
|
|
||||||
icon = R.drawable.ic_poll,
|
|
||||||
iconReference = 1,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.Polls,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.badges,
|
|
||||||
icon = Icons.Outlined.MilitaryTech,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.Badges,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.discover_marketplace,
|
|
||||||
icon = Icons.Outlined.Storefront,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.Products,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavigationRow(
|
|
||||||
title = R.string.emoji_sets,
|
|
||||||
icon = Icons.Outlined.EmojiEmotions,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.BrowseEmojiSets,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
CollapsibleSection(title = R.string.drawer_section_create) {
|
CollapsibleSection(title = R.string.drawer_section_create) {
|
||||||
NavigationRow(
|
NavigationRow(
|
||||||
@@ -746,13 +577,9 @@ fun ListContent(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
NavigationRow(
|
NavBarCatalog[NavBarItem.SETTINGS]?.let {
|
||||||
title = R.string.settings,
|
CatalogNavigationRow(it, MaterialTheme.colorScheme.onBackground, accountViewModel, nav)
|
||||||
icon = Icons.Outlined.Settings,
|
}
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
nav = nav,
|
|
||||||
route = Route.AllSettings,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
@@ -766,6 +593,61 @@ fun ListContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a drawer section by iterating [ids] and looking each one up in [NavBarCatalog].
|
||||||
|
* Profile gets the primary-colored tint; every other item uses onBackground.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun CatalogSection(
|
||||||
|
titleRes: Int,
|
||||||
|
ids: List<NavBarItem>,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val primary = MaterialTheme.colorScheme.primary
|
||||||
|
val onBackground = MaterialTheme.colorScheme.onBackground
|
||||||
|
|
||||||
|
CollapsibleSection(title = titleRes) {
|
||||||
|
ids.forEach { id ->
|
||||||
|
NavBarCatalog[id]?.let { def ->
|
||||||
|
val tint = if (def.id == NavBarItem.PROFILE) primary else onBackground
|
||||||
|
CatalogNavigationRow(def, tint, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CatalogNavigationRow(
|
||||||
|
def: NavBarItemDef,
|
||||||
|
tint: Color,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
when (val icon = def.icon) {
|
||||||
|
is NavBarIcon.Drawable -> {
|
||||||
|
NavigationRow(
|
||||||
|
title = def.labelRes,
|
||||||
|
icon = icon.resId,
|
||||||
|
iconReference = icon.reference,
|
||||||
|
tint = tint,
|
||||||
|
nav = nav,
|
||||||
|
computeRoute = { def.resolveRoute(accountViewModel) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is NavBarIcon.Vector -> {
|
||||||
|
NavigationRow(
|
||||||
|
title = def.labelRes,
|
||||||
|
icon = icon.vector,
|
||||||
|
tint = tint,
|
||||||
|
nav = nav,
|
||||||
|
computeRoute = { def.resolveRoute(accountViewModel) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CollapsibleSection(
|
fun CollapsibleSection(
|
||||||
title: Int,
|
title: Int,
|
||||||
@@ -821,6 +703,27 @@ fun NavigationRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavigationRow(
|
||||||
|
title: Int,
|
||||||
|
icon: Int,
|
||||||
|
iconReference: Int,
|
||||||
|
tint: Color,
|
||||||
|
nav: INav,
|
||||||
|
computeRoute: () -> Route,
|
||||||
|
) {
|
||||||
|
IconRow(
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
iconReference,
|
||||||
|
tint,
|
||||||
|
onClick = {
|
||||||
|
nav.closeDrawer()
|
||||||
|
nav.nav(computeRoute)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationRow(
|
fun NavigationRow(
|
||||||
title: Int,
|
title: Int,
|
||||||
|
|||||||
@@ -209,6 +209,8 @@ sealed class Route {
|
|||||||
|
|
||||||
@Serializable object ReactionsSettings : Route()
|
@Serializable object ReactionsSettings : Route()
|
||||||
|
|
||||||
|
@Serializable object BottomBarSettings : Route()
|
||||||
|
|
||||||
@Serializable object CallSettings : Route()
|
@Serializable object CallSettings : Route()
|
||||||
|
|
||||||
@Serializable object Lists : Route()
|
@Serializable object Lists : Route()
|
||||||
|
|||||||
+8
@@ -32,6 +32,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.outlined.AutoAwesome
|
import androidx.compose.material.icons.outlined.AutoAwesome
|
||||||
import androidx.compose.material.icons.outlined.Bolt
|
import androidx.compose.material.icons.outlined.Bolt
|
||||||
import androidx.compose.material.icons.outlined.CloudUpload
|
import androidx.compose.material.icons.outlined.CloudUpload
|
||||||
|
import androidx.compose.material.icons.outlined.Dashboard
|
||||||
import androidx.compose.material.icons.outlined.DeleteForever
|
import androidx.compose.material.icons.outlined.DeleteForever
|
||||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||||
import androidx.compose.material.icons.outlined.GroupAdd
|
import androidx.compose.material.icons.outlined.GroupAdd
|
||||||
@@ -209,6 +210,13 @@ fun AllSettingsScreen(
|
|||||||
tint = tint,
|
tint = tint,
|
||||||
onClick = { nav.nav(Route.ReactionsSettings) },
|
onClick = { nav.nav(Route.ReactionsSettings) },
|
||||||
)
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
SettingsNavigationRow(
|
||||||
|
title = R.string.bottom_bar_settings,
|
||||||
|
icon = Icons.Outlined.Dashboard,
|
||||||
|
tint = tint,
|
||||||
|
onClick = { nav.nav(Route.BottomBarSettings) },
|
||||||
|
)
|
||||||
HorizontalDivider(thickness = 4.dp)
|
HorizontalDivider(thickness = 4.dp)
|
||||||
SettingsSectionHeader(R.string.danger_zone)
|
SettingsSectionHeader(R.string.danger_zone)
|
||||||
accountViewModel.account.settings.keyPair.privKey?.let {
|
accountViewModel.account.settings.keyPair.privKey?.let {
|
||||||
|
|||||||
+393
@@ -0,0 +1,393 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
|
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.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.DragIndicator
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
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.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarIcon
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItem
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItemDef
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.painterRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview(device = "spec:width=2100px,height=2340px,dpi=440")
|
||||||
|
fun BottomBarSettingsScreenPreview() {
|
||||||
|
ThemeComparisonRow {
|
||||||
|
BottomBarSettingsScreen(
|
||||||
|
mockAccountViewModel(),
|
||||||
|
EmptyNav(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BottomBarSettingsScreen(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopBarWithBackButton(stringRes(id = R.string.bottom_bar_settings), nav::popBack)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
Column(Modifier.padding(padding)) {
|
||||||
|
BottomBarSettingsContent(accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BottomBarSettingsContent(accountViewModel: AccountViewModel) {
|
||||||
|
val pinned by accountViewModel.settings.uiSettingsFlow.bottomBarItems
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
var items by remember(pinned) {
|
||||||
|
mutableStateOf(initialRows(pinned))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(newItems: List<Row>) {
|
||||||
|
items = newItems
|
||||||
|
accountViewModel.settings.uiSettingsFlow.bottomBarItems.tryEmit(
|
||||||
|
newItems.filter { it.pinned }.map { it.item },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var draggedItemIndex by remember { mutableIntStateOf(-1) }
|
||||||
|
var dragOffset by remember { mutableFloatStateOf(0f) }
|
||||||
|
val itemHeights = remember { mutableStateMapOf<Int, Float>() }
|
||||||
|
val isDragging = draggedItemIndex >= 0
|
||||||
|
val scrollState = remember { ScrollState(0) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState, enabled = !isDragging),
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.bottom_bar_settings_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color.Gray,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp, start = Size20dp, end = Size20dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
items.forEachIndexed { index, row ->
|
||||||
|
val rowIsDragging = draggedItemIndex == index
|
||||||
|
val targetElevation = if (rowIsDragging) 8f else 0f
|
||||||
|
val animatedElevation by animateFloatAsState(
|
||||||
|
targetValue = targetElevation,
|
||||||
|
label = "dragElevation",
|
||||||
|
)
|
||||||
|
|
||||||
|
NavBarItemCard(
|
||||||
|
row = row,
|
||||||
|
isDragging = rowIsDragging,
|
||||||
|
canDrag = row.pinned,
|
||||||
|
dragOffsetY = if (rowIsDragging) dragOffset else 0f,
|
||||||
|
elevation = animatedElevation,
|
||||||
|
onTogglePinned = {
|
||||||
|
val newItems = items.toMutableList()
|
||||||
|
val toggled = row.copy(pinned = !row.pinned)
|
||||||
|
newItems.removeAt(index)
|
||||||
|
val insertIndex =
|
||||||
|
if (toggled.pinned) {
|
||||||
|
newItems.indexOfFirst { !it.pinned }.let { if (it < 0) newItems.size else it }
|
||||||
|
} else {
|
||||||
|
val firstUnpinned = newItems.indexOfFirst { !it.pinned }
|
||||||
|
if (firstUnpinned < 0) newItems.size else firstUnpinned
|
||||||
|
}
|
||||||
|
newItems.add(insertIndex, toggled)
|
||||||
|
save(newItems)
|
||||||
|
},
|
||||||
|
onMeasured = { height ->
|
||||||
|
itemHeights[index] = height
|
||||||
|
},
|
||||||
|
onDragStart = {
|
||||||
|
draggedItemIndex = index
|
||||||
|
dragOffset = 0f
|
||||||
|
},
|
||||||
|
onDrag = { dragAmount ->
|
||||||
|
dragOffset += dragAmount
|
||||||
|
|
||||||
|
val currentIndex = draggedItemIndex
|
||||||
|
if (currentIndex < 0) return@NavBarItemCard
|
||||||
|
|
||||||
|
// Can only swap among pinned items (row.pinned == true).
|
||||||
|
if (dragOffset < 0 && currentIndex > 0 && items[currentIndex - 1].pinned) {
|
||||||
|
val aboveHeight = itemHeights[currentIndex - 1] ?: 0f
|
||||||
|
if (-dragOffset > aboveHeight / 2f) {
|
||||||
|
val newItems = items.toMutableList()
|
||||||
|
val temp = newItems[currentIndex - 1]
|
||||||
|
newItems[currentIndex - 1] = newItems[currentIndex]
|
||||||
|
newItems[currentIndex] = temp
|
||||||
|
items = newItems
|
||||||
|
|
||||||
|
val h1 = itemHeights[currentIndex]
|
||||||
|
val h2 = itemHeights[currentIndex - 1]
|
||||||
|
if (h1 != null) itemHeights[currentIndex - 1] = h1
|
||||||
|
if (h2 != null) itemHeights[currentIndex] = h2
|
||||||
|
|
||||||
|
dragOffset += aboveHeight
|
||||||
|
draggedItemIndex = currentIndex - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragOffset > 0 &&
|
||||||
|
currentIndex < items.lastIndex &&
|
||||||
|
items[currentIndex + 1].pinned
|
||||||
|
) {
|
||||||
|
val belowHeight = itemHeights[currentIndex + 1] ?: 0f
|
||||||
|
if (dragOffset > belowHeight / 2f) {
|
||||||
|
val newItems = items.toMutableList()
|
||||||
|
val temp = newItems[currentIndex + 1]
|
||||||
|
newItems[currentIndex + 1] = newItems[currentIndex]
|
||||||
|
newItems[currentIndex] = temp
|
||||||
|
items = newItems
|
||||||
|
|
||||||
|
val h1 = itemHeights[currentIndex]
|
||||||
|
val h2 = itemHeights[currentIndex + 1]
|
||||||
|
if (h1 != null) itemHeights[currentIndex + 1] = h1
|
||||||
|
if (h2 != null) itemHeights[currentIndex] = h2
|
||||||
|
|
||||||
|
dragOffset -= belowHeight
|
||||||
|
draggedItemIndex = currentIndex + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd = {
|
||||||
|
draggedItemIndex = -1
|
||||||
|
dragOffset = 0f
|
||||||
|
save(items)
|
||||||
|
},
|
||||||
|
onDragCancel = {
|
||||||
|
draggedItemIndex = -1
|
||||||
|
dragOffset = 0f
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.zIndex(if (rowIsDragging) 1f else 0f),
|
||||||
|
)
|
||||||
|
|
||||||
|
val nextIsFirstUnpinned =
|
||||||
|
index < items.lastIndex && row.pinned && !items[index + 1].pinned
|
||||||
|
if (nextIsFirstUnpinned) {
|
||||||
|
SectionDivider(R.string.bottom_bar_settings_available)
|
||||||
|
} else if (index < items.lastIndex) {
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class Row(
|
||||||
|
val item: NavBarItem,
|
||||||
|
val pinned: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun initialRows(pinned: List<NavBarItem>): List<Row> {
|
||||||
|
val pinnedRows = pinned.mapNotNull { id -> NavBarCatalog[id]?.let { Row(id, pinned = true) } }
|
||||||
|
val unpinnedRows =
|
||||||
|
NavBarCatalog.keys
|
||||||
|
.filter { it !in pinned }
|
||||||
|
.map { Row(it, pinned = false) }
|
||||||
|
return pinnedRows + unpinnedRows
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SectionDivider(titleRes: Int) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(titleRes),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 20.dp, bottom = 4.dp, start = Size20dp, end = Size20dp),
|
||||||
|
)
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavBarItemCard(
|
||||||
|
row: Row,
|
||||||
|
isDragging: Boolean,
|
||||||
|
canDrag: Boolean,
|
||||||
|
dragOffsetY: Float,
|
||||||
|
elevation: Float,
|
||||||
|
onTogglePinned: () -> Unit,
|
||||||
|
onMeasured: (Float) -> Unit,
|
||||||
|
onDragStart: () -> Unit,
|
||||||
|
onDrag: (Float) -> Unit,
|
||||||
|
onDragEnd: () -> Unit,
|
||||||
|
onDragCancel: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val def = NavBarCatalog[row.item] ?: return
|
||||||
|
val label = stringRes(def.labelRes)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
onMeasured(coordinates.size.height.toFloat())
|
||||||
|
}.graphicsLayer {
|
||||||
|
translationY = dragOffsetY
|
||||||
|
shadowElevation = elevation
|
||||||
|
if (isDragging) {
|
||||||
|
scaleX = 1.02f
|
||||||
|
scaleY = 1.02f
|
||||||
|
}
|
||||||
|
}.padding(vertical = 8.dp, horizontal = Size20dp)
|
||||||
|
.then(
|
||||||
|
if (canDrag) {
|
||||||
|
Modifier.pointerInput(Unit) {
|
||||||
|
detectDragGestures(
|
||||||
|
onDragStart = { onDragStart() },
|
||||||
|
onDrag = { change, dragAmount ->
|
||||||
|
change.consume()
|
||||||
|
onDrag(dragAmount.y)
|
||||||
|
},
|
||||||
|
onDragEnd = { onDragEnd() },
|
||||||
|
onDragCancel = { onDragCancel() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
NavBarIconBox(def)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
|
||||||
|
Switch(
|
||||||
|
checked = row.pinned,
|
||||||
|
onCheckedChange = { onTogglePinned() },
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(28.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (canDrag) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.DragIndicator,
|
||||||
|
contentDescription = stringRes(R.string.bottom_bar_settings_reorder),
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavBarIconBox(def: NavBarItemDef) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(28.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
val description = stringRes(def.labelRes)
|
||||||
|
val tint = MaterialTheme.colorScheme.onBackground
|
||||||
|
when (val icon = def.icon) {
|
||||||
|
is NavBarIcon.Drawable -> {
|
||||||
|
Icon(
|
||||||
|
painter = painterRes(icon.resId, icon.reference),
|
||||||
|
contentDescription = description,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = tint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is NavBarIcon.Vector -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon.vector,
|
||||||
|
contentDescription = description,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = tint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -369,6 +369,7 @@
|
|||||||
<string name="private_conversation_notification">"<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s."</string>
|
<string name="private_conversation_notification">"<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s."</string>
|
||||||
<string name="account_switch_add_account_dialog_title">Add New Account</string>
|
<string name="account_switch_add_account_dialog_title">Add New Account</string>
|
||||||
<string name="drawer_accounts">Accounts</string>
|
<string name="drawer_accounts">Accounts</string>
|
||||||
|
<string name="drawer_section_navigate">Navigate</string>
|
||||||
<string name="drawer_section_you">You</string>
|
<string name="drawer_section_you">You</string>
|
||||||
<string name="drawer_section_feeds">Feeds</string>
|
<string name="drawer_section_feeds">Feeds</string>
|
||||||
<string name="drawer_section_create">Create</string>
|
<string name="drawer_section_create">Create</string>
|
||||||
@@ -1554,6 +1555,10 @@
|
|||||||
|
|
||||||
<string name="change_reaction">Change Quick Reactions</string>
|
<string name="change_reaction">Change Quick Reactions</string>
|
||||||
|
|
||||||
|
<string name="bottom_bar_settings">Bottom Navigation Bar</string>
|
||||||
|
<string name="bottom_bar_settings_description">Drag to reorder. Toggle to add or remove an item from the bottom bar. With zero items the bottom bar is hidden.</string>
|
||||||
|
<string name="bottom_bar_settings_available">Available</string>
|
||||||
|
<string name="bottom_bar_settings_reorder">Reorder</string>
|
||||||
<string name="reactions_settings">Reaction Row</string>
|
<string name="reactions_settings">Reaction Row</string>
|
||||||
<string name="reactions_settings_description">Configure which reaction buttons are shown, their order, and whether to display counters.</string>
|
<string name="reactions_settings_description">Configure which reaction buttons are shown, their order, and whether to display counters.</string>
|
||||||
<string name="reactions_settings_enabled">Enabled</string>
|
<string name="reactions_settings_enabled">Enabled</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user