290 lines
9.6 KiB
Kotlin
290 lines
9.6 KiB
Kotlin
/*
|
|
* 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.common.posts
|
|
|
|
import androidx.compose.animation.Crossfade
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
import androidx.compose.foundation.layout.FlowRow
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.absoluteOffset
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.height
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.requiredSize
|
|
import androidx.compose.foundation.layout.width
|
|
import androidx.compose.foundation.layout.wrapContentHeight
|
|
import androidx.compose.foundation.shape.CircleShape
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.automirrored.filled.Comment
|
|
import androidx.compose.material.icons.filled.AccountCircle
|
|
import androidx.compose.material.icons.filled.Favorite
|
|
import androidx.compose.material.icons.filled.FavoriteBorder
|
|
import androidx.compose.material3.Badge
|
|
import androidx.compose.material3.BadgedBox
|
|
import androidx.compose.material3.HorizontalDivider
|
|
import androidx.compose.material3.Icon
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Text
|
|
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.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.platform.testTag
|
|
import androidx.compose.ui.semantics.Role
|
|
import androidx.compose.ui.text.AnnotatedString
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.unit.dp
|
|
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
|
import dev.msfjarvis.claw.common.ui.NetworkImage
|
|
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
|
import dev.msfjarvis.claw.model.LinkMetadata
|
|
import dev.msfjarvis.claw.model.UIPost
|
|
import kotlinx.collections.immutable.ImmutableList
|
|
import kotlinx.collections.immutable.toImmutableList
|
|
|
|
@Composable
|
|
fun LobstersCard(
|
|
post: UIPost,
|
|
postActions: PostActions,
|
|
refresh: () -> Unit,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
var localReadState by remember(post) { mutableStateOf(post.isRead) }
|
|
var localSavedState by remember(post) { mutableStateOf(post.isSaved) }
|
|
Box(
|
|
modifier =
|
|
modifier
|
|
.fillMaxWidth()
|
|
.clickable {
|
|
postActions.viewPost(post.shortId, post.url, post.commentsUrl)
|
|
localReadState = true
|
|
refresh()
|
|
}
|
|
.background(MaterialTheme.colorScheme.background)
|
|
.padding(start = 16.dp, top = 16.dp, end = 4.dp, bottom = 16.dp)
|
|
) {
|
|
Row(
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
) {
|
|
PostDetails(post = post, isRead = localReadState, modifier = Modifier.weight(1f))
|
|
Column(
|
|
modifier = Modifier.wrapContentHeight(),
|
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
) {
|
|
SaveButton(
|
|
isSaved = localSavedState,
|
|
modifier =
|
|
Modifier.clickable(role = Role.Button) {
|
|
localSavedState = !localSavedState
|
|
postActions.toggleSave(post)
|
|
refresh()
|
|
},
|
|
)
|
|
HorizontalDivider(modifier = Modifier.width(48.dp))
|
|
CommentsButton(
|
|
commentCount = post.commentCount,
|
|
modifier =
|
|
Modifier.clickable(
|
|
role = Role.Button,
|
|
onClick = {
|
|
postActions.viewComments(post.shortId)
|
|
localReadState = true
|
|
refresh()
|
|
},
|
|
),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun PostDetails(post: UIPost, isRead: Boolean, modifier: Modifier = Modifier) {
|
|
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
PostTitle(title = post.title, isRead = isRead)
|
|
TagRow(tags = post.tags.toImmutableList())
|
|
Spacer(Modifier.height(4.dp))
|
|
Submitter(
|
|
text = AnnotatedString("Submitted by ${post.submitter}"),
|
|
avatarUrl = "https://lobste.rs/avatars/${post.submitter}-100.png",
|
|
contentDescription = "User avatar for ${post.submitter}",
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
internal fun PostTitle(title: String, isRead: Boolean, modifier: Modifier = Modifier) {
|
|
Text(
|
|
text = title,
|
|
modifier = modifier,
|
|
style = MaterialTheme.typography.titleMedium,
|
|
fontWeight = if (isRead) FontWeight.Normal else FontWeight.Bold,
|
|
color = MaterialTheme.colorScheme.onBackground,
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
internal fun Submitter(
|
|
text: AnnotatedString,
|
|
avatarUrl: String,
|
|
contentDescription: String,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
Row(
|
|
modifier = modifier,
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
) {
|
|
NetworkImage(
|
|
url = avatarUrl,
|
|
placeholder = Icons.Filled.AccountCircle,
|
|
contentDescription = contentDescription,
|
|
modifier = Modifier.requiredSize(24.dp).clip(CircleShape),
|
|
placeholderTintColor = MaterialTheme.colorScheme.onBackground,
|
|
)
|
|
|
|
Text(
|
|
text = text,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = MaterialTheme.colorScheme.onBackground,
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun SaveButton(isSaved: Boolean, modifier: Modifier = Modifier) {
|
|
Crossfade(targetState = isSaved, label = "save-button") { saved ->
|
|
Box(modifier = modifier.padding(12.dp)) {
|
|
Icon(
|
|
imageVector = if (saved) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
|
tint = MaterialTheme.colorScheme.secondary,
|
|
contentDescription = if (saved) "Remove from saved posts" else "Add to saved posts",
|
|
modifier = Modifier.align(Alignment.Center).testTag("save_button"),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun CommentsButton(commentCount: Int?, modifier: Modifier = Modifier) {
|
|
BadgedBox(
|
|
modifier = modifier.padding(12.dp),
|
|
badge = {
|
|
if (commentCount != null) {
|
|
Badge(
|
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
|
// Required to make the badge label look visually okay
|
|
modifier = Modifier.absoluteOffset(y = (-8).dp),
|
|
) {
|
|
Text(
|
|
text = commentCount.toString(),
|
|
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
|
style = MaterialTheme.typography.labelMedium,
|
|
)
|
|
}
|
|
}
|
|
},
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.AutoMirrored.Filled.Comment,
|
|
tint = MaterialTheme.colorScheme.secondary,
|
|
contentDescription = "Open comments",
|
|
modifier = Modifier.testTag("comments_button"),
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
@OptIn(ExperimentalLayoutApi::class)
|
|
internal fun TagRow(tags: ImmutableList<String>, modifier: Modifier = Modifier) {
|
|
FlowRow(
|
|
modifier = modifier,
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
) {
|
|
tags.forEach { tag -> TagText(tag = tag) }
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun TagText(tag: String, modifier: Modifier = Modifier) {
|
|
Text(
|
|
text = tag,
|
|
modifier =
|
|
modifier
|
|
.background(MaterialTheme.colorScheme.tertiaryContainer, CircleShape)
|
|
.padding(vertical = 4.dp, horizontal = 12.dp),
|
|
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
|
style = MaterialTheme.typography.labelLarge,
|
|
)
|
|
}
|
|
|
|
@ThemePreviews
|
|
@Composable
|
|
private fun LobstersCardPreview() {
|
|
LobstersTheme {
|
|
LobstersCard(
|
|
post =
|
|
UIPost(
|
|
shortId = "ooga",
|
|
title = "Simple Anomaly Detection Using Plain SQL",
|
|
url = "https://hakibenita.com/sql-anomaly-detection",
|
|
createdAt = "2020-09-21T08:04:24.000-05:00",
|
|
commentCount = 1,
|
|
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
|
|
submitter = "Haki",
|
|
tags = listOf("databases", "apis"),
|
|
description = "",
|
|
isSaved = true,
|
|
isRead = true,
|
|
),
|
|
postActions =
|
|
object : PostActions {
|
|
override fun viewPost(postId: String, postUrl: String, commentsUrl: String) {}
|
|
|
|
override fun viewComments(postId: String) {}
|
|
|
|
override fun viewCommentsPage(commentsUrl: String) {}
|
|
|
|
override fun toggleSave(post: UIPost) {}
|
|
|
|
override suspend fun getComments(postId: String): UIPost {
|
|
return UIPost(
|
|
shortId = "ooga",
|
|
title = "Simple Anomaly Detection Using Plain SQL",
|
|
url = "https://hakibenita.com/sql-anomaly-detection",
|
|
createdAt = "2020-09-21T08:04:24.000-05:00",
|
|
commentCount = 1,
|
|
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
|
|
tags = listOf("databases", "apis"),
|
|
description = "",
|
|
submitter = "Haki",
|
|
comments = emptyList(),
|
|
)
|
|
}
|
|
|
|
override suspend fun getLinkMetadata(url: String): LinkMetadata {
|
|
return LinkMetadata("", "")
|
|
}
|
|
},
|
|
refresh = {},
|
|
)
|
|
}
|
|
}
|