diff --git a/android/proguard-rules-missing-classes.pro b/android/proguard-rules-missing-classes.pro index 9898440a..e2476ae6 100644 --- a/android/proguard-rules-missing-classes.pro +++ b/android/proguard-rules-missing-classes.pro @@ -9,3 +9,16 @@ -dontwarn org.openjsse.javax.net.ssl.SSLParameters -dontwarn org.openjsse.javax.net.ssl.SSLSocket -dontwarn org.openjsse.net.ssl.OpenJSSE +-dontwarn androidx.window.extensions.WindowExtensions +-dontwarn androidx.window.extensions.WindowExtensionsProvider +-dontwarn androidx.window.extensions.area.ExtensionWindowAreaPresentation +-dontwarn androidx.window.extensions.layout.DisplayFeature +-dontwarn androidx.window.extensions.layout.FoldingFeature +-dontwarn androidx.window.extensions.layout.WindowLayoutComponent +-dontwarn androidx.window.extensions.layout.WindowLayoutInfo +-dontwarn androidx.window.sidecar.SidecarDeviceState +-dontwarn androidx.window.sidecar.SidecarDisplayFeature +-dontwarn androidx.window.sidecar.SidecarInterface$SidecarCallback +-dontwarn androidx.window.sidecar.SidecarInterface +-dontwarn androidx.window.sidecar.SidecarProvider +-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/ext.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/ext.kt index e3be0465..df3137a1 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/ext.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/ext.kt @@ -94,7 +94,7 @@ fun TwoPaneLayoutPostActions( override fun viewComments(postId: String) { viewModel.markPostAsRead(postId) - setSelectedPost(postId) // Update selectedPostId + setSelectedPost(postId) } override fun viewCommentsPage(post: UIPost) { diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPosts.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPosts.kt index 1a505e93..78ebc62a 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPosts.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPosts.kt @@ -7,7 +7,6 @@ package dev.msfjarvis.claw.android.ui.lists import androidx.activity.compose.ReportDrawnWhen -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -51,7 +50,6 @@ fun NetworkPosts( postActions: PostActions, contentPadding: PaddingValues, modifier: Modifier = Modifier, - onPostClick: (String) -> Unit, ) { ReportDrawnWhen { lazyPagingItems.itemCount > 0 } val refreshLoadState by rememberUpdatedState(lazyPagingItems.loadState.refresh) @@ -84,14 +82,7 @@ fun NetworkPosts( ) { index -> val item = lazyPagingItems[index] if (item != null) { - LobstersListItem( - item = item, - postActions = postActions, - modifier = - Modifier.clickable { - onPostClick(item.shortId) // Trigger the click listener - }, - ) + LobstersListItem(item = item, postActions = postActions) HorizontalDivider() } } @@ -121,7 +112,6 @@ private fun ListPreview() { listState = rememberLazyListState(), postActions = TEST_POST_ACTIONS, contentPadding = PaddingValues(), - onPostClick = {}, ) } } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPostsforTwoPaneLayout.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPostsforTwoPaneLayout.kt deleted file mode 100644 index 738be5f9..00000000 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPostsforTwoPaneLayout.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright © 2021-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.android.ui.lists - -import androidx.activity.compose.ReportDrawnWhen -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.paging.LoadState -import androidx.paging.PagingData -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemContentType -import androidx.paging.compose.itemKey -import dev.msfjarvis.claw.common.posts.PostActions -import dev.msfjarvis.claw.common.posts.TEST_POST -import dev.msfjarvis.claw.common.posts.TEST_POST_ACTIONS -import dev.msfjarvis.claw.common.theme.LobstersTheme -import dev.msfjarvis.claw.common.ui.NetworkError -import dev.msfjarvis.claw.common.ui.ProgressBar -import dev.msfjarvis.claw.common.ui.preview.DevicePreviews -import dev.msfjarvis.claw.model.UIPost -import kotlinx.coroutines.flow.MutableStateFlow - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun NetworkPostsForTwoPaneLayout( - lazyPagingItems: LazyPagingItems, - listState: LazyListState, - postActions: PostActions, - contentPadding: PaddingValues, - modifier: Modifier = Modifier, - onPostClick: (String) -> Unit, -) { - ReportDrawnWhen { lazyPagingItems.itemCount > 0 } - val refreshLoadState by rememberUpdatedState(lazyPagingItems.loadState.refresh) - val state = rememberPullToRefreshState() - PullToRefreshBox( - modifier = modifier.fillMaxSize(), - isRefreshing = refreshLoadState == LoadState.Loading, - state = state, - onRefresh = { lazyPagingItems.refresh() }, - indicator = { - PullToRefreshDefaults.Indicator( - state = state, - isRefreshing = refreshLoadState == LoadState.Loading, - modifier = Modifier.align(Alignment.TopCenter).padding(contentPadding), - ) - }, - ) { - if (lazyPagingItems.itemCount == 0 && refreshLoadState is LoadState.Error) { - NetworkError( - label = "Failed to load posts", - error = (refreshLoadState as LoadState.Error).error, - modifier = Modifier.align(Alignment.Center), - ) - } else { - LazyColumn(contentPadding = contentPadding, state = listState) { - items( - count = lazyPagingItems.itemCount, - key = lazyPagingItems.itemKey { it.shortId }, - contentType = lazyPagingItems.itemContentType { "LobstersItem" }, - ) { index -> - val item = lazyPagingItems[index] - if (item != null) { - LobstersListItem( - item = item, - postActions = postActions, - modifier = - Modifier.clickable { - onPostClick(item.shortId) // Trigger the click listener) - }, - ) - HorizontalDivider() - } - } - if (lazyPagingItems.loadState.append == LoadState.Loading) { - item(key = "progressbar") { - ProgressBar( - modifier = - Modifier.fillMaxWidth() - .wrapContentWidth(Alignment.CenterHorizontally) - .padding(vertical = 16.dp) - ) - } - } - } - } - } -} - -@DevicePreviews -@Composable -private fun ListPreview() { - val items = List(20) { TEST_POST.copy(shortId = "${TEST_POST.shortId}${it}") } - val flow = MutableStateFlow(PagingData.from(items)) - LobstersTheme { - NetworkPostsForTwoPaneLayout( - lazyPagingItems = flow.collectAsLazyPagingItems(), - listState = rememberLazyListState(), - postActions = TEST_POST_ACTIONS, - contentPadding = PaddingValues(), - onPostClick = {}, - ) - } -} diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/SearchList.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/SearchList.kt index 909aa917..e7d21bbe 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/SearchList.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/SearchList.kt @@ -33,7 +33,6 @@ fun SearchList( setSearchQuery: (String) -> Unit, contentPadding: PaddingValues, modifier: Modifier = Modifier, - onPostClick: (String) -> Unit, ) { val triggerSearch = { query: String -> setSearchQuery(query) @@ -54,7 +53,6 @@ fun SearchList( listState = listState, postActions = postActions, contentPadding = contentPadding, - onPostClick = onPostClick, ) } } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt index 6bfd00aa..42c11e33 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt @@ -39,7 +39,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.platform.LocalContext @@ -90,7 +89,7 @@ import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch -@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LobstersPostsScreen( urlLauncher: UrlLauncher, @@ -238,7 +237,6 @@ fun LobstersPostsScreen( listState = hottestListState, postActions = postActions, contentPadding = contentPadding, - onPostClick = { postId -> navController.navigate(Comments(postId)) }, ) } composable { @@ -248,7 +246,6 @@ fun LobstersPostsScreen( listState = newestListState, postActions = postActions, contentPadding = contentPadding, - onPostClick = { postId -> navController.navigate(Comments(postId)) }, ) } composable { diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/SearchScreen.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/SearchScreen.kt index ececc2fc..570f80c5 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/SearchScreen.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/SearchScreen.kt @@ -43,7 +43,6 @@ fun SearchScreen( val listState = rememberLazyListState() val searchResults = viewModel.searchResults.collectAsLazyPagingItems() - val onPostClick: (String) -> Unit = { postId -> navController.navigate("comments/$postId") } Scaffold(modifier = modifier) { contentPadding -> NavHost(navController = navController, startDestination = Search) { composable { @@ -55,7 +54,6 @@ fun SearchScreen( searchQuery = viewModel.searchQuery, contentPadding = contentPadding, setSearchQuery = { query -> viewModel.searchQuery = query }, - onPostClick = onPostClick, ) } composable { backStackEntry -> diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/TwoPaneLayout.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/TwoPaneLayout.kt index 6c313d8f..94d613ce 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/TwoPaneLayout.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/TwoPaneLayout.kt @@ -4,8 +4,11 @@ * 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.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -18,13 +21,16 @@ 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.navigation.NavigableListDetailPaneScaffold +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.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -37,14 +43,22 @@ import androidx.paging.compose.collectAsLazyPagingItems import com.deliveryhero.whetstone.compose.injectedViewModel import dev.msfjarvis.claw.android.R import dev.msfjarvis.claw.android.ui.TwoPaneLayoutPostActions -import dev.msfjarvis.claw.android.ui.lists.NetworkPostsForTwoPaneLayout +import dev.msfjarvis.claw.android.ui.lists.NetworkPosts +import dev.msfjarvis.claw.android.ui.navigation.Comments 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.comments.HTMLConverter import dev.msfjarvis.claw.common.urllauncher.UrlLauncher +import kotlinx.coroutines.launch + +private fun ThreePaneScaffoldNavigator<*>.isListExpanded() = + scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded -@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class) +private fun ThreePaneScaffoldNavigator<*>.isDetailExpanded() = + scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded + +@OptIn(ExperimentalMaterial3Api::class) @Composable fun TwoPaneLayout( urlLauncher: UrlLauncher, @@ -52,23 +66,30 @@ fun TwoPaneLayout( modifier: Modifier = Modifier, viewModel: ClawViewModel = injectedViewModel(), ) { - val context = LocalContext.current val hottestListState = rememberLazyListState() val navController = rememberNavController() + val coroutineScope = rememberCoroutineScope() + val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems() + val navigator = rememberListDetailPaneScaffoldNavigator() + val backBehavior = + if (navigator.isListExpanded() && navigator.isDetailExpanded()) { + BackNavigationBehavior.PopUntilContentChange + } else { + BackNavigationBehavior.PopUntilScaffoldValueChange + } - // Track the selected postId as a mutable state - var selectedPostId by remember { mutableStateOf(null) } - - // Initialize postActions with selectedPostId as a mutable state val postActions = remember { - TwoPaneLayoutPostActions(context, urlLauncher, viewModel, { selectedPostId = it }) + TwoPaneLayoutPostActions(context, urlLauncher, viewModel) { + coroutineScope.launch { + navigator.navigateTo(pane = ListDetailPaneScaffoldRole.Detail, contentKey = Comments(it)) + } + } } - val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems() - - // Navigator state - val navigator = rememberListDetailPaneScaffoldNavigator() + BackHandler(navigator.canNavigateBack(backBehavior)) { + coroutineScope.launch { navigator.navigateBack(backBehavior) } + } Scaffold( topBar = { @@ -84,40 +105,46 @@ fun TwoPaneLayout( ) }, content = { paddingValues -> - NavigableListDetailPaneScaffold( + ListDetailPaneScaffold( modifier = modifier.padding(paddingValues), - navigator = navigator, + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, listPane = { - NetworkPostsForTwoPaneLayout( - lazyPagingItems = hottestPosts, - listState = hottestListState, - postActions = postActions, - contentPadding = PaddingValues(), - modifier = Modifier.fillMaxSize(), - ) { postId -> - selectedPostId = postId // Update selectedPostId on click - } - }, - detailPane = { - selectedPostId?.let { postId -> - CommentsPage( - postId = postId, + AnimatedPane { + NetworkPosts( + lazyPagingItems = hottestPosts, + listState = hottestListState, postActions = postActions, - htmlConverter = htmlConverter, - getSeenComments = viewModel::getSeenComments, - markSeenComments = viewModel::markSeenComments, - openUserProfile = { navController.navigate(User(it)) }, contentPadding = PaddingValues(), modifier = Modifier.fillMaxSize(), ) } - ?: Box(Modifier.fillMaxSize()) { - // Placeholder when no post is selected - Text( - text = "Select a post to view comments", - modifier = Modifier.align(Alignment.Center), - ) + }, + 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, + htmlConverter = htmlConverter, + getSeenComments = viewModel::getSeenComments, + markSeenComments = viewModel::markSeenComments, + openUserProfile = { navController.navigate(User(it)) }, + contentPadding = PaddingValues(), + modifier = Modifier.fillMaxSize(), + ) + } } + } }, ) }, diff --git a/android/src/screenshotTest/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPostsTest.kt b/android/src/screenshotTest/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPostsTest.kt index d4757e07..9520ec17 100644 --- a/android/src/screenshotTest/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPostsTest.kt +++ b/android/src/screenshotTest/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPostsTest.kt @@ -31,7 +31,6 @@ class NetworkPostsTest { listState = rememberLazyListState(), postActions = TEST_POST_ACTIONS, contentPadding = PaddingValues(), - onPostClick = {}, ) } }