feat: redesign bottom navigation bar

This commit is contained in:
Harsh Shandilya 2024-08-28 15:16:42 +05:30
parent 4f3bafc051
commit 1de4916c9c
14 changed files with 211 additions and 64 deletions

View file

@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Change submitter text to 'authored' when applicable - Change submitter text to 'authored' when applicable
- Unread comments now have a brighter background rather than a text badge - Unread comments now have a brighter background rather than a text badge
- Bottom navigation's visibility now interacts with the scroll behavior of the post lists - Bottom navigation bar has been redesigned
## [1.48.0] - 2024-06-05 ## [1.48.0] - 2024-06-05

View file

@ -95,6 +95,8 @@ dependencies {
implementation(libs.copydown) implementation(libs.copydown)
implementation(libs.dagger) implementation(libs.dagger)
implementation(libs.eithernet) implementation(libs.eithernet)
implementation(libs.haze)
implementation(libs.haze.materials)
implementation(libs.javax.inject) implementation(libs.javax.inject)
implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)

View file

@ -13,31 +13,37 @@ import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.material3.BottomAppBar import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.BottomAppBarScrollBehavior import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.hazeChild
import dev.msfjarvis.claw.android.ui.navigation.Destination import dev.msfjarvis.claw.android.ui.navigation.Destination
import dev.msfjarvis.claw.android.ui.navigation.matches import dev.msfjarvis.claw.android.ui.navigation.matches
import dev.msfjarvis.claw.common.ui.FloatingNavigationBar
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
const val AnimationDuration = 100 const val AnimationDuration = 100
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ClawNavigationBar( fun ClawNavigationBar(
navController: NavController, navController: NavController,
items: ImmutableList<NavigationItem>, items: ImmutableList<NavigationItem>,
isVisible: Boolean, isVisible: Boolean,
scrollBehavior: BottomAppBarScrollBehavior, hazeState: HazeState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
AnimatedVisibility( AnimatedVisibility(
@ -54,40 +60,60 @@ fun ClawNavigationBar(
targetOffsetY = { fullHeight -> fullHeight }, targetOffsetY = { fullHeight -> fullHeight },
animationSpec = tween(durationMillis = AnimationDuration, easing = FastOutLinearInEasing), animationSpec = tween(durationMillis = AnimationDuration, easing = FastOutLinearInEasing),
), ),
modifier = Modifier, label = "",
) { content = {
BottomAppBar(modifier = modifier, scrollBehavior = scrollBehavior) { FloatingNavigationBar(
val navBackStackEntry = navController.currentBackStackEntryAsState().value tonalElevation = 16.dp,
val currentDestination = navBackStackEntry?.destination shape = MaterialTheme.shapes.extraLarge,
items.forEach { navItem -> modifier =
val isSelected = currentDestination.matches(navItem.destination) modifier
NavigationBarItem( .padding(horizontal = 16.dp)
icon = { .navigationBarsPadding()
Crossfade(isSelected, label = "nav-label") { .clip(MaterialTheme.shapes.extraLarge)
Icon( .hazeChild(
imageVector = if (it) navItem.selectedIcon else navItem.icon, hazeState,
contentDescription = navItem.label.replaceFirstChar(Char::uppercase), style =
) HazeStyle(
} backgroundColor = MaterialTheme.colorScheme.surface,
}, tints = emptyList(),
label = { Text(text = navItem.label) }, blurRadius = 24.dp,
selected = isSelected, noiseFactor = 0f,
onClick = { ),
if (isSelected) { ),
navItem.listStateResetCallback() containerColor = Color.Transparent,
} else { ) {
navController.navigate(navItem.destination) { val navBackStackEntry = navController.currentBackStackEntryAsState().value
popUpTo(navController.graph.startDestinationId) { saveState = true } val currentDestination = navBackStackEntry?.destination
launchSingleTop = true items.forEach { navItem ->
restoreState = true val isSelected = currentDestination.matches(navItem.destination)
NavigationBarItem(
icon = {
Crossfade(isSelected, label = "nav-label") {
Icon(
imageVector = if (it) navItem.selectedIcon else navItem.icon,
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
)
} }
} },
}, label = { Text(text = navItem.label) },
modifier = Modifier.testTag(navItem.label.uppercase()), selected = isSelected,
) onClick = {
if (isSelected) {
navItem.listStateResetCallback()
} else {
navController.navigate(navItem.destination) {
popUpTo(navController.graph.startDestinationId) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
},
modifier = Modifier.testTag(navItem.label.uppercase()),
)
}
} }
} },
} )
} }
class NavigationItem( class NavigationItem(

View file

@ -10,6 +10,7 @@ import androidx.activity.compose.ReportDrawn
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
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.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -36,6 +37,7 @@ fun DatabasePosts(
items: ImmutableMap<String, List<UIPost>>, items: ImmutableMap<String, List<UIPost>>,
listState: LazyListState, listState: LazyListState,
postActions: PostActions, postActions: PostActions,
contentPadding: PaddingValues,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
ReportDrawn() ReportDrawn()
@ -50,7 +52,7 @@ fun DatabasePosts(
Text(text = "No saved posts", style = MaterialTheme.typography.headlineSmall) Text(text = "No saved posts", style = MaterialTheme.typography.headlineSmall)
} }
} else { } else {
LazyColumn(state = listState) { LazyColumn(state = listState, contentPadding = contentPadding) {
items.forEach { (month, posts) -> items.forEach { (month, posts) ->
stickyHeader(contentType = "month-header") { MonthHeader(label = month) } stickyHeader(contentType = "month-header") { MonthHeader(label = month) }
items(items = posts, key = { it.shortId }, contentType = { "LobstersItem" }) { item -> items(items = posts, key = { it.shortId }, contentType = { "LobstersItem" }) { item ->

View file

@ -7,6 +7,7 @@
package dev.msfjarvis.claw.android.ui.lists package dev.msfjarvis.claw.android.ui.lists
import androidx.activity.compose.ReportDrawnWhen import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -47,6 +48,7 @@ fun NetworkPosts(
lazyPagingItems: LazyPagingItems<UIPost>, lazyPagingItems: LazyPagingItems<UIPost>,
listState: LazyListState, listState: LazyListState,
postActions: PostActions, postActions: PostActions,
contentPadding: PaddingValues,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
ReportDrawnWhen { lazyPagingItems.itemCount > 0 } ReportDrawnWhen { lazyPagingItems.itemCount > 0 }
@ -72,7 +74,7 @@ fun NetworkPosts(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
) )
} else { } else {
LazyColumn(state = listState) { LazyColumn(contentPadding = contentPadding, state = listState) {
items( items(
count = lazyPagingItems.itemCount, count = lazyPagingItems.itemCount,
key = lazyPagingItems.itemKey { it.shortId }, key = lazyPagingItems.itemKey { it.shortId },
@ -109,6 +111,7 @@ private fun ListPreview() {
lazyPagingItems = flow.collectAsLazyPagingItems(), lazyPagingItems = flow.collectAsLazyPagingItems(),
listState = rememberLazyListState(), listState = rememberLazyListState(),
postActions = TEST_POST_ACTIONS, postActions = TEST_POST_ACTIONS,
contentPadding = PaddingValues(),
) )
} }
} }

View file

@ -7,6 +7,7 @@
package dev.msfjarvis.claw.android.ui.lists package dev.msfjarvis.claw.android.ui.lists
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
@ -32,6 +33,7 @@ fun SearchList(
searchQuery: String, searchQuery: String,
setSearchQuery: (String) -> Unit, setSearchQuery: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
) { ) {
val lazyPagingItems = items.collectAsLazyPagingItems() val lazyPagingItems = items.collectAsLazyPagingItems()
val triggerSearch = { query: String -> val triggerSearch = { query: String ->
@ -49,6 +51,7 @@ fun SearchList(
lazyPagingItems = lazyPagingItems, lazyPagingItems = lazyPagingItems,
listState = listState, listState = listState,
postActions = postActions, postActions = postActions,
contentPadding = contentPadding,
) )
} }
} }

View file

@ -13,7 +13,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -26,7 +25,6 @@ import androidx.compose.material.icons.filled.Whatshot
import androidx.compose.material.icons.outlined.FavoriteBorder import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material.icons.outlined.NewReleases import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.outlined.Whatshot import androidx.compose.material.icons.outlined.Whatshot
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -45,7 +43,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -61,6 +58,8 @@ import androidx.navigation.toRoute
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.deliveryhero.whetstone.compose.injectedViewModel import com.deliveryhero.whetstone.compose.injectedViewModel
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.haze
import dev.msfjarvis.claw.android.MainActivity import dev.msfjarvis.claw.android.MainActivity
import dev.msfjarvis.claw.android.R import dev.msfjarvis.claw.android.R
import dev.msfjarvis.claw.android.SearchActivity import dev.msfjarvis.claw.android.SearchActivity
@ -111,8 +110,7 @@ fun LobstersPostsScreen(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val postActions = rememberPostActions(context, urlLauncher, navController, viewModel) val postActions = rememberPostActions(context, urlLauncher, navController, viewModel)
val hazeState = remember { HazeState() }
val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems() val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems() val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
@ -209,17 +207,14 @@ fun LobstersPostsScreen(
navController = navController, navController = navController,
items = navItems, items = navItems,
isVisible = currentDestination.any(navDestinations), isVisible = currentDestination.any(navDestinations),
scrollBehavior = scrollBehavior, hazeState = hazeState,
) )
} }
}, },
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
modifier = modifier = modifier.semantics { testTagsAsResourceId = true },
modifier.nestedScroll(scrollBehavior.nestedScrollConnection).semantics { ) { contentPadding ->
testTagsAsResourceId = true Row {
},
) { paddingValues ->
Row(modifier = Modifier.padding(paddingValues)) {
AnimatedVisibility(visible = navigationType == ClawNavigationType.NAVIGATION_RAIL) { AnimatedVisibility(visible = navigationType == ClawNavigationType.NAVIGATION_RAIL) {
ClawNavigationRail( ClawNavigationRail(
navController = navController, navController = navController,
@ -234,6 +229,7 @@ fun LobstersPostsScreen(
// Make animations 2x faster than default specs // Make animations 2x faster than default specs
enterTransition = { fadeIn(animationSpec = tween(350)) }, enterTransition = { fadeIn(animationSpec = tween(350)) },
exitTransition = { fadeOut(animationSpec = tween(350)) }, exitTransition = { fadeOut(animationSpec = tween(350)) },
modifier = Modifier.haze(hazeState),
) { ) {
composable<Hottest> { composable<Hottest> {
setWebUri("https://lobste.rs/") setWebUri("https://lobste.rs/")
@ -241,6 +237,7 @@ fun LobstersPostsScreen(
lazyPagingItems = hottestPosts, lazyPagingItems = hottestPosts,
listState = hottestListState, listState = hottestListState,
postActions = postActions, postActions = postActions,
contentPadding = contentPadding,
) )
} }
composable<Newest> { composable<Newest> {
@ -249,11 +246,17 @@ fun LobstersPostsScreen(
lazyPagingItems = newestPosts, lazyPagingItems = newestPosts,
listState = newestListState, listState = newestListState,
postActions = postActions, postActions = postActions,
contentPadding = contentPadding,
) )
} }
composable<Saved> { composable<Saved> {
setWebUri(null) setWebUri(null)
DatabasePosts(items = savedPosts, listState = savedListState, postActions = postActions) DatabasePosts(
items = savedPosts,
listState = savedListState,
postActions = postActions,
contentPadding = contentPadding,
)
} }
composable<Comments> { backStackEntry -> composable<Comments> { backStackEntry ->
val postId = backStackEntry.toRoute<Comments>().postId val postId = backStackEntry.toRoute<Comments>().postId
@ -264,6 +267,7 @@ fun LobstersPostsScreen(
htmlConverter = htmlConverter, htmlConverter = htmlConverter,
getSeenComments = viewModel::getSeenComments, getSeenComments = viewModel::getSeenComments,
markSeenComments = viewModel::markSeenComments, markSeenComments = viewModel::markSeenComments,
contentPadding = contentPadding,
openUserProfile = { navController.navigate(User(it)) }, openUserProfile = { navController.navigate(User(it)) },
) )
} }
@ -273,6 +277,7 @@ fun LobstersPostsScreen(
UserProfile( UserProfile(
username = username, username = username,
getProfile = viewModel::getUserProfile, getProfile = viewModel::getUserProfile,
contentPadding = contentPadding,
openUserProfile = { navController.navigate(User(it)) }, openUserProfile = { navController.navigate(User(it)) },
) )
} }
@ -284,9 +289,12 @@ fun LobstersPostsScreen(
exportPostsAsJson = viewModel::exportPostsAsJson, exportPostsAsJson = viewModel::exportPostsAsJson,
exportPostsAsHtml = viewModel::exportPostsAsHtml, exportPostsAsHtml = viewModel::exportPostsAsHtml,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
contentPadding = contentPadding,
) )
} }
composable<AboutLibraries> { LibrariesContainer(modifier = Modifier.fillMaxSize()) } composable<AboutLibraries> {
LibrariesContainer(contentPadding = contentPadding, modifier = Modifier.fillMaxSize())
}
} }
} }
} }

View file

@ -6,7 +6,6 @@
*/ */
package dev.msfjarvis.claw.android.ui.screens package dev.msfjarvis.claw.android.ui.screens
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -39,12 +38,8 @@ fun SearchScreen(
val navController = rememberNavController() val navController = rememberNavController()
val postActions = rememberPostActions(LocalContext.current, urlLauncher, navController, viewModel) val postActions = rememberPostActions(LocalContext.current, urlLauncher, navController, viewModel)
val listState = rememberLazyListState() val listState = rememberLazyListState()
Scaffold(modifier = modifier) { paddingValues -> Scaffold(modifier = modifier) { contentPadding ->
NavHost( NavHost(navController = navController, startDestination = Search) {
navController = navController,
startDestination = Search,
modifier = Modifier.padding(paddingValues),
) {
composable<Search> { composable<Search> {
setWebUri("https://lobste.rs/search") setWebUri("https://lobste.rs/search")
SearchList( SearchList(
@ -52,6 +47,7 @@ fun SearchScreen(
listState = listState, listState = listState,
postActions = postActions, postActions = postActions,
searchQuery = viewModel.searchQuery, searchQuery = viewModel.searchQuery,
contentPadding = contentPadding,
setSearchQuery = { query -> viewModel.searchQuery = query }, setSearchQuery = { query -> viewModel.searchQuery = query },
) )
} }
@ -65,6 +61,7 @@ fun SearchScreen(
getSeenComments = viewModel::getSeenComments, getSeenComments = viewModel::getSeenComments,
markSeenComments = viewModel::markSeenComments, markSeenComments = viewModel::markSeenComments,
openUserProfile = { navController.navigate(User(it)) }, openUserProfile = { navController.navigate(User(it)) },
contentPadding = contentPadding,
) )
} }
composable<User> { backStackEntry -> composable<User> { backStackEntry ->
@ -74,6 +71,7 @@ fun SearchScreen(
username = username, username = username,
getProfile = viewModel::getUserProfile, getProfile = viewModel::getUserProfile,
openUserProfile = { navController.navigate(User(it)) }, openUserProfile = { navController.navigate(User(it)) },
contentPadding = contentPadding,
) )
} }
} }

View file

@ -13,11 +13,13 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
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.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.LibraryBooks import androidx.compose.material.icons.automirrored.filled.LibraryBooks
import androidx.compose.material.icons.filled.Bookmarks import androidx.compose.material.icons.filled.Bookmarks
@ -55,10 +57,11 @@ fun SettingsScreen(
importPosts: suspend (InputStream) -> Unit, importPosts: suspend (InputStream) -> Unit,
exportPostsAsJson: suspend (OutputStream) -> Unit, exportPostsAsJson: suspend (OutputStream) -> Unit,
exportPostsAsHtml: suspend (OutputStream) -> Unit, exportPostsAsHtml: suspend (OutputStream) -> Unit,
contentPadding: PaddingValues,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.padding(contentPadding).fillMaxSize()) {
Column { Column {
ListItem( ListItem(
headlineContent = { Text("Data transfer") }, headlineContent = { Text("Data transfer") },

View file

@ -7,6 +7,7 @@
package dev.msfjarvis.claw.common.comments package dev.msfjarvis.claw.common.comments
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -34,6 +35,7 @@ fun CommentsPage(
htmlConverter: HTMLConverter, htmlConverter: HTMLConverter,
getSeenComments: suspend (String) -> PostComments, getSeenComments: suspend (String) -> PostComments,
markSeenComments: (String, List<Comment>) -> Unit, markSeenComments: (String, List<Comment>) -> Unit,
contentPadding: PaddingValues,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
openUserProfile: (String) -> Unit, openUserProfile: (String) -> Unit,
) { ) {
@ -59,6 +61,7 @@ fun CommentsPage(
commentState = commentState, commentState = commentState,
markSeenComments = markSeenComments, markSeenComments = markSeenComments,
openUserProfile = openUserProfile, openUserProfile = openUserProfile,
contentPadding = contentPadding,
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
) )
} }

View file

@ -52,6 +52,7 @@ internal fun CommentsPageInternal(
commentState: PostComments, commentState: PostComments,
markSeenComments: (String, List<Comment>) -> Unit, markSeenComments: (String, List<Comment>) -> Unit,
openUserProfile: (String) -> Unit, openUserProfile: (String) -> Unit,
contentPadding: PaddingValues,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -68,7 +69,7 @@ internal fun CommentsPageInternal(
} }
Surface(color = MaterialTheme.colorScheme.surfaceVariant) { Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) { LazyColumn(modifier = modifier, contentPadding = contentPadding) {
item { item {
CommentsHeader( CommentsHeader(
post = details, post = details,

View file

@ -0,0 +1,94 @@
/*
* Copyright © 2024 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.common.ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BarChart
import androidx.compose.material.icons.filled.BrokenImage
import androidx.compose.material.icons.filled.HeartBroken
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Surface
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
// Taken from Chris Banes' amazing app Tivi
// https://github.com/chrisbanes/tivi/blob/836d596d74959f4235ca2395b5bbfdd6fd9c9a9e/ui/root/src/commonMain/kotlin/app/tivi/home/Home.kt#L173
@Composable
fun FloatingNavigationBar(
modifier: Modifier = Modifier,
shape: Shape = MaterialTheme.shapes.extraLarge,
containerColor: Color = NavigationBarDefaults.containerColor,
contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
tonalElevation: Dp = NavigationBarDefaults.Elevation,
content: @Composable RowScope.() -> Unit,
) {
Surface(
color = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
shape = shape,
border =
BorderStroke(
width = 0.5.dp,
brush =
Brush.verticalGradient(
colors =
listOf(
MaterialTheme.colorScheme.surfaceVariant,
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
)
),
),
modifier = modifier,
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp).fillMaxWidth().height(80.dp).selectableGroup(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
content = content,
)
}
}
@Preview
@Composable
private fun FloatingNavigationBarPreview() {
FloatingNavigationBar {
NavigationBarItem(
selected = true,
onClick = {},
icon = { Icon(imageVector = Icons.Filled.HeartBroken, contentDescription = "Home") },
)
NavigationBarItem(
selected = true,
onClick = {},
icon = { Icon(imageVector = Icons.Filled.BarChart, contentDescription = "Home") },
)
NavigationBarItem(
selected = true,
onClick = {},
icon = { Icon(imageVector = Icons.Filled.BrokenImage, contentDescription = "Home") },
)
}
}

View file

@ -9,6 +9,7 @@ package dev.msfjarvis.claw.common.user
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
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.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.requiredSize
@ -45,6 +46,7 @@ fun UserProfile(
username: String, username: String,
getProfile: suspend (username: String) -> User, getProfile: suspend (username: String) -> User,
openUserProfile: (String) -> Unit, openUserProfile: (String) -> Unit,
contentPadding: PaddingValues,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val user by val user by
@ -67,7 +69,7 @@ fun UserProfile(
} }
is Error -> { is Error -> {
val error = user as Error val error = user as Error
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.padding(contentPadding).fillMaxSize()) {
NetworkError( NetworkError(
label = error.description, label = error.description,
error = error.error, error = error.error,

View file

@ -73,6 +73,8 @@ copydown = "io.github.furstenheim:copy_down:1.1"
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
eithernet = "com.slack.eithernet:eithernet:1.9.0" eithernet = "com.slack.eithernet:eithernet:1.9.0"
haze = "dev.chrisbanes.haze:haze:0.9.0-alpha08"
haze-materials = "dev.chrisbanes.haze:haze-materials:0.9.0-alpha08"
javax-inject = "javax.inject:javax.inject:1" javax-inject = "javax.inject:javax.inject:1"
jsoup = "org.jsoup:jsoup:1.18.1" jsoup = "org.jsoup:jsoup:1.18.1"
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }