diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/main/MainActivity.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/main/MainActivity.kt index 0091a911..8db25a43 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/ui/main/MainActivity.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/main/MainActivity.kt @@ -86,11 +86,13 @@ fun LobstersApp() { posts = hottestPosts, listState = hottestPostsListState, overscrollAction = viewModel::getMorePosts, + saveAction = viewModel::savePost, ) } composable(Destination.Saved.route) { SavedPosts( posts = savedPosts, + saveAction = viewModel::removeSavedPost, ) } } diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/posts/HottestPosts.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/posts/HottestPosts.kt index 6fd3ad5d..78449a0c 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/ui/posts/HottestPosts.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/posts/HottestPosts.kt @@ -15,6 +15,7 @@ fun HottestPosts( listState: LazyListState, modifier: Modifier = Modifier, overscrollAction: () -> Unit, + saveAction: (LobstersPost) -> Unit, ) { val urlLauncher = UrlLauncherAmbient.current @@ -29,10 +30,11 @@ fun HottestPosts( if (posts.lastIndex == index) { overscrollAction.invoke() } - LobstersItem( + LobstersItemRedux( post = item, - linkOpenAction = { post -> urlLauncher.launch(post.url.ifEmpty { post.commentsUrl }) }, - commentOpenAction = { post -> urlLauncher.launch(post.commentsUrl) }, + onClick = { post -> urlLauncher.launch(post.url.ifEmpty { post.commentsUrl }) }, + onLongClick = { post -> urlLauncher.launch(post.commentsUrl) }, + onSaveButtonClick = saveAction, ) } } diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/posts/LobstersItemRedux.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/posts/LobstersItemRedux.kt new file mode 100644 index 00000000..68915435 --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/posts/LobstersItemRedux.kt @@ -0,0 +1,187 @@ +package dev.msfjarvis.lobsters.ui.posts + +import androidx.compose.foundation.Text +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.ConstrainedLayoutReference +import androidx.compose.foundation.layout.ConstraintLayout +import androidx.compose.foundation.layout.ConstraintLayoutScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumnFor +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.material.ripple.RippleIndication +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.ui.tooling.preview.Preview +import coil.transform.CircleCropTransformation +import dev.chrisbanes.accompanist.coil.CoilImage +import dev.msfjarvis.lobsters.R +import dev.msfjarvis.lobsters.model.LobstersPost +import dev.msfjarvis.lobsters.model.Submitter +import dev.msfjarvis.lobsters.ui.theme.LobstersTheme +import dev.msfjarvis.lobsters.ui.theme.titleColor +import dev.msfjarvis.lobsters.util.IconResource + +val TEST_POST = LobstersPost( + "zqyydb", + "https://lobste.rs/s/zqyydb", + "2020-09-21T07:11:14.000-05:00", + "k2k20 hackathon report: Bob Beck on LibreSSL progress", + "https://undeadly.org/cgi?action=article;sid=20200921105847", + 4, + 0, + 0, + "", + "https://lobste.rs/s/zqyydb/k2k20_hackathon_report_bob_beck_on", + Submitter( + "Vigdis", + "2017-02-27T21:08:14.000-06:00", + false, + "Alleycat for the fun, sys/net admin for a living and OpenBSD contributions for the pleasure. (Not so) French dude in Montreal\r\n\r\nhttps://chown.me", + false, + 76, + "/avatars/Vigdis-100.png", + "sevan", + null, + null, + emptyList(), + ), + listOf("openbsd", "linux", "containers", "hack the planet", "no thanks"), +) + +@Composable +fun LobstersItemRedux( + post: LobstersPost, + onClick: (LobstersPost) -> Unit = {}, + onLongClick: (LobstersPost) -> Unit = {}, + onSaveButtonClick: (LobstersPost) -> Unit = {}, +) { + Surface( + modifier = Modifier.fillMaxWidth() + .clickable( + onClick = { onClick.invoke(post) }, + onLongClick = { onLongClick.invoke(post) }, + ), + ) { + ConstraintLayout( + modifier = Modifier.padding(start = 4.dp, end = 4.dp), + ) { + val (title, tags, avatar, submitter, saveButton) = createRefs() + Text( + text = post.title, + color = titleColor, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(top = 4.dp) + .constrainAs(title) { + top.linkTo(parent.top) + start.linkTo(parent.start) + }, + ) + TagRow( + tags = post.tags, + modifier = Modifier.constrainAs(tags) { + top.linkTo(title.bottom) + }.padding(vertical = 8.dp), + ) + CoilImage( + data = "https://lobste.rs/${post.submitterUser.avatarUrl}", + fadeIn = true, + requestBuilder = { + transformations(CircleCropTransformation()) + }, + modifier = Modifier.width(30.dp).padding(4.dp) + .constrainAs(avatar) { + top.linkTo(tags.bottom) + start.linkTo(parent.start) + }, + ) + Text( + text = "submitted by ${post.submitterUser.username}", + modifier = Modifier.padding(bottom = 4.dp).constrainAs(submitter) { + top.linkTo(tags.bottom) + start.linkTo(avatar.end) + }, + ) + IconResource( + resourceId = R.drawable.ic_favorite_border_24px, + modifier = Modifier.padding(8.dp) + .clickable( + onClick = { onSaveButtonClick.invoke(post) }, + indication = RippleIndication(), + ) + .constrainAs(saveButton) { + end.linkTo(parent.end) + centerVerticallyTo(parent) + }, + tint = Color(0xFFD97373), + ) + } + } +} + +/** + * This [Composable] function generates a row-like view of tags using [ConstraintLayout]. Since we + * don't know before hand how many items we will have, we generate refs ahead of time, take the + * first 4 elements from [tags] and map nullable values out of that list. This lets us manually + * add [Text] elements based on how many tags we actually have. Since we're constraining manually + * as opposed to using some reflection hack, the resulting code is a lot of copy and pasting. + */ +@Composable +fun ConstraintLayoutScope.TagRow( + tags: List, + modifier: Modifier = Modifier, +) { + val (tag1ref, tag2ref, tag3ref, tag4ref) = remember { createRefs() } + val clampedTags = remember(tags) { (0..3).map(tags::elementAtOrNull).toList() } + clampedTags[0]?.let { tag -> + Tag(tag, modifier, tag1ref, null) + } + clampedTags[1]?.let { tag -> + Tag(tag, modifier, tag2ref, tag1ref) + } + clampedTags[2]?.let { tag -> + Tag(tag, modifier, tag3ref, tag2ref) + } + clampedTags[3]?.let { tag -> + Tag(tag, modifier, tag4ref, tag3ref) + } +} + +@Composable +fun ConstraintLayoutScope.Tag( + text: String, + modifier: Modifier, + currentRef: ConstrainedLayoutReference, + previousRef: ConstrainedLayoutReference?, +) { + Text( + text = text, + modifier = Modifier + .then(modifier) + .constrainAs(currentRef) { + start.linkTo(previousRef?.end ?: parent.start) + } + .background(Color(0xFFFFFCD7), RoundedCornerShape(8.dp)) + .padding(vertical = 2.dp, horizontal = 6.dp), + color = Color.DarkGray, + ) +} + +@Composable +@Preview +fun Preview() { + LobstersTheme { + LazyColumnFor(items = listOf(TEST_POST)) { item -> + LobstersItemRedux( + post = item, + ) + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/posts/SavedPosts.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/posts/SavedPosts.kt index 27319df7..30154262 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/ui/posts/SavedPosts.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/posts/SavedPosts.kt @@ -13,6 +13,7 @@ import dev.msfjarvis.lobsters.ui.urllauncher.UrlLauncherAmbient fun SavedPosts( posts: List, modifier: Modifier = Modifier, + saveAction: (LobstersPost) -> Unit, ) { val listState = rememberLazyListState() val urlLauncher = UrlLauncherAmbient.current @@ -25,10 +26,11 @@ fun SavedPosts( state = listState, modifier = Modifier.padding(horizontal = 8.dp).then(modifier) ) { item -> - LobstersItem( + LobstersItemRedux( post = item, - linkOpenAction = { post -> urlLauncher.launch(post.url.ifEmpty { post.commentsUrl }) }, - commentOpenAction = { post -> urlLauncher.launch(post.commentsUrl) }, + onClick = { post -> urlLauncher.launch(post.url.ifEmpty { post.commentsUrl }) }, + onLongClick = { post -> urlLauncher.launch(post.commentsUrl) }, + onSaveButtonClick = saveAction, ) } }