feat: rewrite navigation on top of Nav3

This commit is contained in:
Harsh Shandilya 2025-05-25 13:25:57 +05:30
parent 9b322c212f
commit 17289a26f1
9 changed files with 115 additions and 582 deletions

View file

@ -107,6 +107,8 @@ dependencies {
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.lifecycle.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.profileinstaller)
implementation(libs.androidx.work.runtime)

View file

@ -33,19 +33,6 @@
</intent-filter>
</activity>
<activity
android:name=".SearchActivity"
android:configChanges="colorMode|density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
android:exported="true"
android:label="Claw Search"
android:launchMode="singleTask"
android:taskAffinity=""
android:theme="@style/Theme.Claw">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"

View file

@ -6,16 +6,13 @@
*/
package dev.msfjarvis.claw.android
import androidx.compose.foundation.layout.fillMaxSize
import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
import dev.msfjarvis.claw.android.ui.screens.LobstersPostsScreen
import dev.msfjarvis.claw.android.ui.screens.TabletScreen
import dev.msfjarvis.claw.android.ui.screens.Nav3Screen
@ContributesActivityInjector
class MainActivity : BaseActivity() {
@ -24,24 +21,16 @@ class MainActivity : BaseActivity() {
@Composable
override fun Content() {
val windowSizeClass = calculateWindowSizeClass(this)
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> {
LobstersPostsScreen(
Nav3Screen(
urlLauncher = urlLauncher,
windowSizeClass = windowSizeClass,
setWebUri = { url -> webUri = url },
)
}
else -> {
TabletScreen(urlLauncher = urlLauncher, modifier = Modifier.fillMaxSize())
}
}
}
override fun preLaunch() {
super.preLaunch()
enableEdgeToEdge()
installSplashScreen()
}

View file

@ -1,19 +0,0 @@
/*
* Copyright © 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.android
import androidx.compose.runtime.Composable
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
import dev.msfjarvis.claw.android.ui.screens.SearchScreen
@ContributesActivityInjector
class SearchActivity : BaseActivity() {
@Composable
override fun Content() {
SearchScreen(urlLauncher = urlLauncher, setWebUri = { webUri = it }, viewModel = viewModel)
}
}

View file

@ -20,21 +20,19 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.snapshots.SnapshotStateList
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.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import dev.chrisbanes.haze.HazeDefaults
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.hazeEffect
import dev.msfjarvis.claw.android.ui.navigation.AppDestinations
import dev.msfjarvis.claw.android.ui.navigation.Destination
import dev.msfjarvis.claw.android.ui.navigation.matches
import dev.msfjarvis.claw.common.ui.FloatingNavigationBar
import kotlinx.collections.immutable.ImmutableList
@ -42,7 +40,7 @@ const val AnimationDuration = 100
@Composable
fun ClawNavigationBar(
navController: NavController,
backStack: SnapshotStateList<Destination>,
items: ImmutableList<NavigationItem>,
isVisible: Boolean,
hazeState: HazeState,
@ -85,10 +83,9 @@ fun ClawNavigationBar(
containerColor =
if (HazeDefaults.blurEnabled()) Color.Transparent else MaterialTheme.colorScheme.surface,
) {
val navBackStackEntry = navController.currentBackStackEntryAsState().value
val currentDestination = navBackStackEntry?.destination
val currentDestination = backStack.firstOrNull()
items.forEach { navItem ->
val isSelected = currentDestination.matches(navItem.destination)
val isSelected = currentDestination == navItem.destination
NavigationBarItem(
icon = {
Crossfade(isSelected, label = "nav-label") {
@ -104,11 +101,7 @@ fun ClawNavigationBar(
if (isSelected) {
navItem.listStateResetCallback()
} else {
navController.navigate(navItem.destination) {
popUpTo(navController.graph.startDestinationId) { saveState = true }
launchSingleTop = true
restoreState = true
}
backStack.add(navItem.destination)
}
},
modifier = Modifier.testTag(navItem.label.uppercase()),

View file

@ -6,13 +6,8 @@
*/
package dev.msfjarvis.claw.android.ui.screens
import android.content.Intent
import androidx.activity.compose.LocalActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.rememberLazyListState
@ -32,6 +27,7 @@ import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
@ -44,22 +40,17 @@ import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import androidx.navigation3.runtime.entry
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import androidx.paging.compose.collectAsLazyPagingItems
import com.deliveryhero.whetstone.compose.injectedViewModel
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeSource
import dev.msfjarvis.claw.android.MainActivity
import dev.msfjarvis.claw.android.R
import dev.msfjarvis.claw.android.SearchActivity
import dev.msfjarvis.claw.android.ui.PostActions
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
@ -67,9 +58,11 @@ import dev.msfjarvis.claw.android.ui.navigation.AboutLibraries
import dev.msfjarvis.claw.android.ui.navigation.AppDestinations
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
import dev.msfjarvis.claw.android.ui.navigation.Comments
import dev.msfjarvis.claw.android.ui.navigation.Destination
import dev.msfjarvis.claw.android.ui.navigation.Hottest
import dev.msfjarvis.claw.android.ui.navigation.Newest
import dev.msfjarvis.claw.android.ui.navigation.Saved
import dev.msfjarvis.claw.android.ui.navigation.Search
import dev.msfjarvis.claw.android.ui.navigation.Settings
import dev.msfjarvis.claw.android.ui.navigation.User
import dev.msfjarvis.claw.android.ui.navigation.any
@ -85,26 +78,23 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LobstersPostsScreen(
fun Nav3Screen(
urlLauncher: UrlLauncher,
windowSizeClass: WindowSizeClass,
setWebUri: (String?) -> Unit,
modifier: Modifier = Modifier,
viewModel: ClawViewModel = injectedViewModel(),
) {
val backStack = remember { mutableStateListOf<Destination>(Hottest) }
// region Pain
val context = LocalContext.current
val activity = LocalActivity.current
val hottestListState = rememberLazyListState()
val newestListState = rememberLazyListState()
val savedListState = rememberLazyListState()
val navController = rememberNavController()
val navBackStackEntry = navController.currentBackStackEntryAsState().value
val currentDestination = navBackStackEntry?.destination
val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val postActions = remember {
PostActions(context, urlLauncher, viewModel) { navController.navigate(Comments(it)) }
}
val hazeState = remember { HazeState() }
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
@ -116,7 +106,7 @@ fun LobstersPostsScreen(
val postIdOverride = activity?.intent?.extras?.getString(MainActivity.NAVIGATION_KEY)
LaunchedEffect(Unit) {
if (postIdOverride != null) {
navController.navigate(Comments(postIdOverride))
backStack.add(Comments(postIdOverride))
}
}
@ -137,16 +127,19 @@ fun LobstersPostsScreen(
},
)
val navDestinations = navItems.map(NavigationItem::destination).toPersistentList()
// endregion
val postActions = remember {
PostActions(context, urlLauncher, viewModel) { backStack.add(Comments(it)) }
}
Scaffold(
topBar = {
TopAppBar(
modifier = Modifier.shadow(8.dp),
navigationIcon = {
if (
navController.previousBackStackEntry != null && currentDestination.none(navDestinations)
) {
IconButton(onClick = { if (!navController.popBackStack()) activity?.finish() }) {
if (backStack.none { it in navDestinations }) {
IconButton(onClick = { if (backStack.removeLastOrNull() == null) activity?.finish() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Go back to previous screen",
@ -161,18 +154,16 @@ fun LobstersPostsScreen(
}
},
title = {
if (currentDestination.any(navDestinations)) {
if (backStack.any { it in navDestinations }) {
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
}
},
actions = {
if (currentDestination.any(navDestinations)) {
IconButton(
onClick = { context.startActivity(Intent(context, SearchActivity::class.java)) }
) {
if (backStack.any { it in navDestinations }) {
IconButton(onClick = { backStack.add(Search) }) {
Icon(imageVector = Icons.Filled.Search, contentDescription = "Search posts")
}
IconButton(onClick = { navController.navigate(Settings) }) {
IconButton(onClick = { backStack.add(Settings) }) {
Icon(imageVector = Icons.Filled.Tune, contentDescription = "Settings")
}
}
@ -182,34 +173,23 @@ fun LobstersPostsScreen(
bottomBar = {
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
ClawNavigationBar(
navController = navController,
backStack,
items = navItems,
isVisible = currentDestination.any(navDestinations),
isVisible = backStack.any { it in navDestinations },
hazeState = hazeState,
)
}
},
snackbarHost = { SnackbarHost(snackbarHostState) },
modifier = modifier.semantics { testTagsAsResourceId = true },
modifier = Modifier.semantics { testTagsAsResourceId = true },
) { contentPadding ->
Row {
AnimatedVisibility(visible = navigationType == ClawNavigationType.NAVIGATION_RAIL) {
ClawNavigationRail(
navController = navController,
items = navItems,
isVisible = currentDestination.any(navDestinations),
)
}
NavHost(
navController = navController,
startDestination = Hottest,
// Make animations 2x faster than default specs
enterTransition = { fadeIn(animationSpec = tween(350)) },
exitTransition = { fadeOut(animationSpec = tween(350)) },
modifier = Modifier.hazeSource(hazeState),
) {
composable<Hottest> {
NavDisplay(
backStack = backStack,
modifier = modifier,
onBack = { backStack.removeLastOrNull() },
entryProvider =
entryProvider {
entry<Hottest> {
setWebUri("https://lobste.rs/")
NetworkPosts(
lazyPagingItems = hottestPosts,
@ -218,7 +198,7 @@ fun LobstersPostsScreen(
contentPadding = contentPadding,
)
}
composable<Newest> {
entry<Newest> {
setWebUri("https://lobste.rs/")
NetworkPosts(
lazyPagingItems = newestPosts,
@ -227,7 +207,7 @@ fun LobstersPostsScreen(
contentPadding = contentPadding,
)
}
composable<Saved> {
entry<Saved> {
setWebUri(null)
DatabasePosts(
items = savedPosts,
@ -236,33 +216,11 @@ fun LobstersPostsScreen(
contentPadding = contentPadding,
)
}
composable<Comments> { backStackEntry ->
val postId = backStackEntry.toRoute<Comments>().postId
setWebUri("https://lobste.rs/s/$postId")
CommentsPage(
postId = postId,
postActions = postActions,
getSeenComments = viewModel::getSeenComments,
markSeenComments = viewModel::markSeenComments,
contentPadding = contentPadding,
openUserProfile = { navController.navigate(User(it)) },
)
}
composable<User> { backStackEntry ->
val username = backStackEntry.toRoute<User>().username
setWebUri("https://lobste.rs/u/$username")
UserProfile(
username = username,
getProfile = viewModel::getUserProfile,
contentPadding = contentPadding,
openUserProfile = { navController.navigate(User(it)) },
)
}
composable<Settings> {
entry<Settings> {
SettingsScreen(
openInputStream = context.contentResolver::openInputStream,
openOutputStream = context.contentResolver::openOutputStream,
openLibrariesScreen = { navController.navigate(AboutLibraries) },
openLibrariesScreen = { backStack.add(AboutLibraries) },
importPosts = viewModel::importPosts,
exportPostsAsJson = viewModel::exportPostsAsJson,
exportPostsAsHtml = viewModel::exportPostsAsHtml,
@ -271,10 +229,31 @@ fun LobstersPostsScreen(
modifier = Modifier.fillMaxSize(),
)
}
composable<AboutLibraries> {
entry<Comments> { dest ->
CommentsPage(
postId = dest.postId,
postActions = postActions,
getSeenComments = viewModel::getSeenComments,
markSeenComments = viewModel::markSeenComments,
contentPadding = contentPadding,
openUserProfile = { backStack.add(User(it)) },
)
}
entry<User> { dest ->
UserProfile(
username = dest.username,
getProfile = viewModel::getUserProfile,
contentPadding = contentPadding,
openUserProfile = { backStack.add(User(it)) },
)
}
entry<Search> {
SearchScreen(urlLauncher = urlLauncher, setWebUri = setWebUri, viewModel = viewModel)
}
entry<AboutLibraries> {
LibrariesContainer(contentPadding = contentPadding, modifier = Modifier.fillMaxSize())
}
}
}
},
)
}
}

View file

@ -1,234 +0,0 @@
/*
* Copyright © 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.
*/
@file:OptIn(ExperimentalMaterial3AdaptiveApi::class)
package dev.msfjarvis.claw.android.ui.screens
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.paging.compose.collectAsLazyPagingItems
import com.deliveryhero.whetstone.compose.injectedViewModel
import dev.msfjarvis.claw.android.R
import dev.msfjarvis.claw.android.ui.PostActions
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
import dev.msfjarvis.claw.android.ui.navigation.AppDestinations
import dev.msfjarvis.claw.android.ui.navigation.Comments
import dev.msfjarvis.claw.android.ui.navigation.Hottest
import dev.msfjarvis.claw.android.ui.navigation.Newest
import dev.msfjarvis.claw.android.ui.navigation.Saved
import dev.msfjarvis.claw.android.ui.navigation.User
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
import dev.msfjarvis.claw.common.comments.CommentsPage
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.launch
private fun ThreePaneScaffoldNavigator<*>.isListExpanded() =
scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
private fun ThreePaneScaffoldNavigator<*>.isDetailExpanded() =
scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TabletScreen(
urlLauncher: UrlLauncher,
modifier: Modifier = Modifier,
viewModel: ClawViewModel = injectedViewModel(),
) {
val context = LocalContext.current
val hottestListState = rememberLazyListState()
val newestListState = rememberLazyListState()
val savedListState = rememberLazyListState()
val navController = rememberNavController()
val coroutineScope = rememberCoroutineScope()
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
val savedPosts by viewModel.savedPostsByMonth.collectAsStateWithLifecycle(persistentMapOf())
val navigator = rememberListDetailPaneScaffoldNavigator<Comments>()
val backBehavior =
if (navigator.isListExpanded() && navigator.isDetailExpanded()) {
BackNavigationBehavior.PopUntilContentChange
} else {
BackNavigationBehavior.PopUntilScaffoldValueChange
}
val postActions = remember {
PostActions(context, urlLauncher, viewModel) {
coroutineScope.launch {
navigator.navigateTo(pane = ListDetailPaneScaffoldRole.Detail, contentKey = Comments(it))
}
}
}
val navItems =
persistentListOf(
NavigationItem(AppDestinations.HOTTEST) {
coroutineScope.launch {
if (hottestPosts.itemCount > 0) hottestListState.animateScrollToItem(index = 0)
}
},
NavigationItem(AppDestinations.NEWEST) {
coroutineScope.launch {
if (newestPosts.itemCount > 0) newestListState.animateScrollToItem(index = 0)
}
},
NavigationItem(AppDestinations.SAVED) {
coroutineScope.launch {
if (savedPosts.isNotEmpty()) savedListState.animateScrollToItem(index = 0)
}
},
)
BackHandler(navigator.canNavigateBack(backBehavior)) {
coroutineScope.launch { navigator.navigateBack(backBehavior) }
}
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
if (navigator.canNavigateBack(backBehavior)) {
IconButton(
onClick = { coroutineScope.launch { navigator.navigateBack(backBehavior) } }
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Go back to previous screen",
)
}
} else {
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = "The app icon for Claw",
modifier = Modifier.size(48.dp),
)
}
},
title = {
if (!navigator.canNavigateBack(backBehavior)) {
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
}
},
)
},
content = { paddingValues ->
Row {
ClawNavigationRail(navController = navController, items = navItems, isVisible = true)
ListDetailPaneScaffold(
modifier = modifier.padding(paddingValues),
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
AnimatedPane {
NavHost(
navController = navController,
startDestination = Hottest,
enterTransition = { fadeIn(tween(350)) },
exitTransition = { fadeOut(tween(350)) },
) {
composable<Hottest> {
NetworkPosts(
lazyPagingItems = hottestPosts,
listState = hottestListState,
postActions = postActions,
contentPadding = PaddingValues(),
)
}
composable<Newest> {
NetworkPosts(
lazyPagingItems = newestPosts,
listState = newestListState,
postActions = postActions,
contentPadding = PaddingValues(),
)
}
composable<Saved> {
DatabasePosts(
items = savedPosts,
listState = savedListState,
postActions = postActions,
contentPadding = PaddingValues(),
)
}
}
}
},
detailPane = {
AnimatedPane {
when (val contentKey = navigator.currentDestination?.contentKey) {
null -> {
Box(Modifier.fillMaxSize()) {
Text(
text = "Select a post to view comments",
modifier = Modifier.align(Alignment.Center),
)
}
}
else -> {
CommentsPage(
postId = contentKey.postId,
postActions = postActions,
getSeenComments = viewModel::getSeenComments,
markSeenComments = viewModel::markSeenComments,
contentPadding = PaddingValues(),
modifier = Modifier.fillMaxSize(),
openUserProfile = { navController.navigate(User(it)) },
)
}
}
}
},
)
}
},
)
}

View file

@ -1,167 +0,0 @@
/*
* Copyright © 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.android.ui.screens
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.compose.collectAsLazyPagingItems
import com.deliveryhero.whetstone.compose.injectedViewModel
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import dev.msfjarvis.claw.android.ui.PostActions
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
import dev.msfjarvis.claw.android.ui.navigation.AboutLibraries
import dev.msfjarvis.claw.android.ui.navigation.AppDestinations
import dev.msfjarvis.claw.android.ui.navigation.Comments
import dev.msfjarvis.claw.android.ui.navigation.Hottest
import dev.msfjarvis.claw.android.ui.navigation.Newest
import dev.msfjarvis.claw.android.ui.navigation.Saved
import dev.msfjarvis.claw.android.ui.navigation.Settings
import dev.msfjarvis.claw.android.ui.navigation.User
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
import dev.msfjarvis.claw.common.comments.CommentsPage
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
import dev.msfjarvis.claw.common.user.UserProfile
import kotlinx.collections.immutable.persistentMapOf
@Composable
fun TabletScreen2(
urlLauncher: UrlLauncher,
setWebUri: (String?) -> Unit,
modifier: Modifier = Modifier,
viewModel: ClawViewModel = injectedViewModel(),
) {
// TODO: Needs a custom Saver implementation, should probably be an ArrayDeque
val navigationBackStack = rememberSaveable {
mutableStateListOf(AppDestinations.HOTTEST.destination)
}
val context = LocalContext.current
val hottestListState = rememberLazyListState()
val newestListState = rememberLazyListState()
val savedListState = rememberLazyListState()
val snackbarHostState = remember { SnackbarHostState() }
val postActions = remember {
PostActions(context, urlLauncher, viewModel) { navigationBackStack.add(Comments(it)) }
}
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
val savedPosts by viewModel.savedPostsByMonth.collectAsStateWithLifecycle(persistentMapOf())
BackHandler(navigationBackStack.size > 1) {
navigationBackStack.removeAt(navigationBackStack.size - 1)
}
val contentPadding = PaddingValues()
NavigationSuiteScaffold(
navigationSuiteItems = {
AppDestinations.entries.forEach {
item(
icon = { Icon(imageVector = it.icon, contentDescription = it.label) },
selected = it.destination == navigationBackStack.first(),
onClick = { navigationBackStack.add(it.destination) },
)
}
},
modifier = modifier,
) {
when (navigationBackStack.first()) {
AboutLibraries -> {
LibrariesContainer(contentPadding = contentPadding, modifier = Modifier.fillMaxSize())
}
Hottest -> {
setWebUri("https://lobste.rs/")
NetworkPosts(
lazyPagingItems = hottestPosts,
listState = hottestListState,
postActions = postActions,
contentPadding = contentPadding,
)
}
Newest -> {
setWebUri("https://lobste.rs/")
NetworkPosts(
lazyPagingItems = newestPosts,
listState = newestListState,
postActions = postActions,
contentPadding = contentPadding,
)
}
Saved -> {
setWebUri(null)
DatabasePosts(
items = savedPosts,
listState = savedListState,
postActions = postActions,
contentPadding = contentPadding,
)
}
Settings -> {
setWebUri(null)
SettingsScreen(
openInputStream = context.contentResolver::openInputStream,
openOutputStream = context.contentResolver::openOutputStream,
openLibrariesScreen = { navigationBackStack.add(AboutLibraries) },
importPosts = viewModel::importPosts,
exportPostsAsJson = viewModel::exportPostsAsJson,
exportPostsAsHtml = viewModel::exportPostsAsHtml,
snackbarHostState = snackbarHostState,
contentPadding = contentPadding,
modifier = Modifier.fillMaxSize(),
)
}
is Comments -> {
val postId = (navigationBackStack.first() as Comments).postId
setWebUri("https://lobste.rs/s/$postId")
CommentsPage(
postId = postId,
postActions = postActions,
getSeenComments = viewModel::getSeenComments,
markSeenComments = viewModel::markSeenComments,
contentPadding = contentPadding,
openUserProfile = { navigationBackStack.add(User(it)) },
)
}
is User -> {
val username = (navigationBackStack.first() as User).username
setWebUri("https://lobste.rs/u/$username")
UserProfile(
username = username,
getProfile = viewModel::getUserProfile,
contentPadding = contentPadding,
openUserProfile = { navigationBackStack.add(User(it)) },
)
}
else -> {
Box(Modifier.fillMaxSize()) {
Text(
text = "Unexpected destination: ${navigationBackStack.first()}, please report this bug",
modifier = Modifier.align(Alignment.Center),
)
}
}
}
}
}

View file

@ -17,6 +17,7 @@ kotlinResult = "2.0.1"
leakcanary = "3.0-alpha-8"
lifecycle = "2.9.0"
navigation = "2.9.0"
navigation3 = "1.0.0-alpha02"
retrofit = "3.0.0"
richtext = "1.0.0-alpha02"
sentry-sdk = "8.12.0"
@ -55,6 +56,8 @@ androidx-core-splashscreen = "androidx.core:core-splashscreen:1.2.0-beta02"
androidx-lifecycle-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-lint-gradle = "androidx.lint:lint-gradle:1.0.0-alpha05"
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
androidx-paging-compose = "androidx.paging:paging-compose:3.3.6"
androidx-profileinstaller = "androidx.profileinstaller:profileinstaller:1.4.1"
androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" }