mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 19:57:04 +05:30
common: migrate to Android-only
This commit is contained in:
parent
47a00ad61a
commit
ab2713154f
55 changed files with 43 additions and 474 deletions
2
common/src/main/AndroidManifest.xml
Normal file
2
common/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<manifest />
|
|
@ -0,0 +1,7 @@
|
|||
package dev.msfjarvis.claw.common
|
||||
|
||||
sealed class NetworkState {
|
||||
class Success<T>(val data: T) : NetworkState()
|
||||
class Error(val message: String) : NetworkState()
|
||||
object Loading : NetworkState()
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package dev.msfjarvis.claw.common.comments
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
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.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.posts.PostTitle
|
||||
import dev.msfjarvis.claw.common.posts.Submitter
|
||||
import dev.msfjarvis.claw.common.posts.TagRow
|
||||
import dev.msfjarvis.claw.common.res.ClawIcons
|
||||
import dev.msfjarvis.claw.common.ui.ThemedRichText
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
|
||||
@Composable
|
||||
fun CommentsHeader(
|
||||
postDetails: LobstersPostDetails,
|
||||
postActions: PostActions,
|
||||
) {
|
||||
val htmlConverter = LocalHTMLConverter.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
PostTitle(title = postDetails.title)
|
||||
TagRow(tags = postDetails.tags)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
if (postDetails.url.isNotBlank()) {
|
||||
PostLink(
|
||||
link = postDetails.url,
|
||||
modifier =
|
||||
Modifier.clickable { postActions.viewPost(postDetails.url, postDetails.commentsUrl) },
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
if (postDetails.description.isNotBlank()) {
|
||||
ThemedRichText(htmlConverter.convertHTMLToMarkdown(postDetails.description))
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
Submitter(
|
||||
text = "Submitted by ${postDetails.submitter.username}",
|
||||
avatarUrl = "https://lobste.rs/${postDetails.submitter.avatarUrl}",
|
||||
contentDescription = "User avatar for ${postDetails.submitter.username}",
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
uriHandler.openUri("https://lobste.rs/u/${postDetails.submitter.username}")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PostLink(
|
||||
link: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier.background(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
) {
|
||||
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Icon(
|
||||
painter = ClawIcons.Web,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSecondary,
|
||||
)
|
||||
Text(
|
||||
text = link,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
color = MaterialTheme.colorScheme.onSecondary,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun CommentEntry(
|
||||
comment: Comment,
|
||||
) {
|
||||
var expanded by remember(comment) { mutableStateOf(true) }
|
||||
val htmlConverter = LocalHTMLConverter.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable { expanded = !expanded }
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(start = (comment.indentLevel * 16).dp, end = 16.dp, top = 16.dp, bottom = 16.dp),
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Submitter(
|
||||
text = comment.user.username,
|
||||
avatarUrl = "https://lobste.rs/${comment.user.avatarUrl}",
|
||||
contentDescription = "User avatar for ${comment.user.username}",
|
||||
modifier =
|
||||
Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${comment.user.username}") },
|
||||
)
|
||||
AnimatedContent(targetState = expanded) { expandedState ->
|
||||
if (expandedState) {
|
||||
ThemedRichText(
|
||||
text = htmlConverter.convertHTMLToMarkdown(comment.comment),
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package dev.msfjarvis.claw.common.comments
|
||||
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.NetworkState
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.ui.Divider
|
||||
import dev.msfjarvis.claw.common.ui.NetworkError
|
||||
import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
|
||||
@Composable
|
||||
private fun CommentsPageInternal(
|
||||
details: LobstersPostDetails,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
|
||||
LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) {
|
||||
item { CommentsHeader(postDetails = details, postActions = postActions) }
|
||||
|
||||
if (details.commentCount > 0) {
|
||||
item {
|
||||
Text(
|
||||
text = "Comments",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
)
|
||||
}
|
||||
|
||||
itemsIndexed(details.comments) { index, item ->
|
||||
if (index != 0) {
|
||||
Divider()
|
||||
}
|
||||
CommentEntry(item)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
Text(
|
||||
text = "No Comments",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
fun CommentsPage(
|
||||
postId: String,
|
||||
getDetails: suspend (String) -> LobstersPostDetails,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val postDetails by
|
||||
produceState<NetworkState>(NetworkState.Loading) {
|
||||
value = NetworkState.Success(getDetails(postId))
|
||||
}
|
||||
|
||||
when (postDetails) {
|
||||
is NetworkState.Success<*> -> {
|
||||
CommentsPageInternal(
|
||||
(postDetails as NetworkState.Success<LobstersPostDetails>).data,
|
||||
postActions,
|
||||
modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
is NetworkState.Error -> {
|
||||
NetworkError((postDetails as NetworkState.Error).message)
|
||||
}
|
||||
NetworkState.Loading -> ProgressBar(modifier)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package dev.msfjarvis.claw.common.comments
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
|
||||
/** Defines a contract to convert strings of HTML to Markdown. */
|
||||
fun interface HTMLConverter {
|
||||
fun convertHTMLToMarkdown(html: String): String
|
||||
}
|
||||
|
||||
val LocalHTMLConverter = staticCompositionLocalOf<HTMLConverter> { error("To be provided") }
|
|
@ -0,0 +1,229 @@
|
|||
package dev.msfjarvis.claw.common.posts
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
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.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import dev.msfjarvis.claw.common.res.ClawIcons
|
||||
import dev.msfjarvis.claw.common.ui.Divider
|
||||
import dev.msfjarvis.claw.common.ui.NetworkImage
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun LobstersCard(
|
||||
post: SavedPost,
|
||||
isSaved: Boolean,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var localSavedState by remember(post, isSaved) { mutableStateOf(isSaved) }
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { postActions.viewPost(post.url, post.commentsUrl) }
|
||||
.padding(start = 16.dp, top = 16.dp, end = 4.dp, bottom = 16.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
PostDetails(
|
||||
modifier = Modifier.weight(1f),
|
||||
post = post,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SaveButton(
|
||||
isSaved = localSavedState,
|
||||
modifier =
|
||||
Modifier.clickable(
|
||||
role = Role.Button,
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
localSavedState = !localSavedState
|
||||
postActions.toggleSave(post)
|
||||
},
|
||||
)
|
||||
Divider(modifier = Modifier.width(48.dp))
|
||||
CommentsButton(
|
||||
commentCount = post.commentCount,
|
||||
modifier =
|
||||
Modifier.combinedClickable(
|
||||
role = Role.Button,
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { postActions.viewComments(post.shortId) },
|
||||
onLongClick = { postActions.viewCommentsPage(post.commentsUrl) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PostDetails(post: SavedPost, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PostTitle(title = post.title)
|
||||
TagRow(tags = post.tags)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Submitter(
|
||||
text = "Submitted by ${post.submitterName}",
|
||||
avatarUrl = "https://lobste.rs/${post.submitterAvatarUrl}",
|
||||
contentDescription = "User avatar for ${post.submitterName}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PostTitle(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = modifier,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Submitter(
|
||||
text: String,
|
||||
avatarUrl: String,
|
||||
contentDescription: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
NetworkImage(
|
||||
url = avatarUrl,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier.requiredSize(24.dp).clip(CircleShape),
|
||||
)
|
||||
|
||||
Text(text = text, modifier = modifier, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SaveButton(
|
||||
isSaved: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Crossfade(targetState = isSaved) { saved ->
|
||||
Box(modifier = modifier.padding(12.dp)) {
|
||||
Icon(
|
||||
painter = if (saved) ClawIcons.Heart else ClawIcons.HeartBorder,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
contentDescription = if (saved) "Remove from saved posts" else "Add to saved posts",
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CommentsButton(
|
||||
commentCount: Int?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BadgedBox(
|
||||
modifier = modifier.padding(12.dp),
|
||||
badge = {
|
||||
if (commentCount != null) {
|
||||
Badge(
|
||||
modifier = Modifier.absoluteOffset(x = (-8).dp),
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
) {
|
||||
Text(
|
||||
text = commentCount.toString(),
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = ClawIcons.Comment,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
contentDescription = "Open comments",
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TagRow(
|
||||
tags: List<String>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
mainAxisSpacing = 8.dp,
|
||||
crossAxisSpacing = 8.dp,
|
||||
) {
|
||||
tags.forEach { tag -> TagText(tag) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TagText(
|
||||
tag: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = tag,
|
||||
modifier =
|
||||
Modifier.background(MaterialTheme.colorScheme.tertiaryContainer, RoundedCornerShape(50))
|
||||
.padding(vertical = 4.dp, horizontal = 12.dp)
|
||||
.then(modifier),
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package dev.msfjarvis.claw.common.posts
|
||||
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
|
||||
interface PostActions {
|
||||
fun viewPost(postUrl: String, commentsUrl: String)
|
||||
fun viewComments(postId: String)
|
||||
fun viewCommentsPage(commentsUrl: String)
|
||||
fun toggleSave(post: SavedPost)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package dev.msfjarvis.claw.common.posts
|
||||
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
|
||||
fun LobstersPost.toDbModel(): SavedPost {
|
||||
return SavedPost(
|
||||
shortId = shortId,
|
||||
title = title,
|
||||
url = url,
|
||||
createdAt = createdAt,
|
||||
commentCount = commentCount,
|
||||
commentsUrl = commentsUrl,
|
||||
submitterName = submitter.username,
|
||||
submitterAvatarUrl = submitter.avatarUrl,
|
||||
tags = tags,
|
||||
)
|
||||
}
|
||||
|
||||
fun LobstersPostDetails.toDbModel(): SavedPost {
|
||||
return SavedPost(
|
||||
shortId = shortId,
|
||||
title = title,
|
||||
url = url,
|
||||
createdAt = createdAt,
|
||||
commentCount = commentCount,
|
||||
commentsUrl = commentsUrl,
|
||||
submitterName = submitter.username,
|
||||
submitterAvatarUrl = submitter.avatarUrl,
|
||||
tags = tags,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package dev.msfjarvis.claw.common.res
|
||||
|
||||
import dev.msfjarvis.claw.common.res.clawicons.arrow_back_black_24dp
|
||||
import dev.msfjarvis.claw.common.res.clawicons.comment_black_24dp
|
||||
import dev.msfjarvis.claw.common.res.clawicons.favorite_black_24dp
|
||||
import dev.msfjarvis.claw.common.res.clawicons.favorite_border_black_24dp
|
||||
import dev.msfjarvis.claw.common.res.clawicons.new_releases_black_24dp
|
||||
import dev.msfjarvis.claw.common.res.clawicons.new_releases_filled_black_24dp
|
||||
import dev.msfjarvis.claw.common.res.clawicons.public_black_24dp
|
||||
import dev.msfjarvis.claw.common.res.clawicons.whatshot_black_24dp
|
||||
import dev.msfjarvis.claw.common.res.clawicons.whatshot_filled_black_24dp
|
||||
|
||||
object ClawIcons {
|
||||
|
||||
val ArrowBack = arrow_back_black_24dp()
|
||||
|
||||
val Comment = comment_black_24dp()
|
||||
|
||||
val Flame = whatshot_black_24dp()
|
||||
|
||||
val FlameFilled = whatshot_filled_black_24dp()
|
||||
|
||||
val Heart = favorite_black_24dp()
|
||||
|
||||
val HeartBorder = favorite_border_black_24dp()
|
||||
|
||||
val New = new_releases_black_24dp()
|
||||
|
||||
val NewFilled = new_releases_filled_black_24dp()
|
||||
|
||||
val Web = public_black_24dp()
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package dev.msfjarvis.claw.common.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val md_theme_light_primary = Color(0xFFac3325)
|
||||
val md_theme_light_onPrimary = Color(0xFFffffff)
|
||||
val md_theme_light_primaryContainer = Color(0xFFffdad3)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF410000)
|
||||
val md_theme_light_secondary = Color(0xFF775752)
|
||||
val md_theme_light_onSecondary = Color(0xFFffffff)
|
||||
val md_theme_light_secondaryContainer = Color(0xFFffdad3)
|
||||
val md_theme_light_onSecondaryContainer = Color(0xFF2c1511)
|
||||
val md_theme_light_tertiary = Color(0xFF705c2e)
|
||||
val md_theme_light_onTertiary = Color(0xFFffffff)
|
||||
val md_theme_light_tertiaryContainer = Color(0xFFfbdfa5)
|
||||
val md_theme_light_onTertiaryContainer = Color(0xFF261a00)
|
||||
val md_theme_light_error = Color(0xFFba1b1b)
|
||||
val md_theme_light_errorContainer = Color(0xFFffdad4)
|
||||
val md_theme_light_onError = Color(0xFFffffff)
|
||||
val md_theme_light_onErrorContainer = Color(0xFF410001)
|
||||
val md_theme_light_background = Color(0xFFfcfcfc)
|
||||
val md_theme_light_onBackground = Color(0xFF201a19)
|
||||
val md_theme_light_surface = Color(0xFFfcfcfc)
|
||||
val md_theme_light_onSurface = Color(0xFF201a19)
|
||||
val md_theme_light_surfaceVariant = Color(0xFFf5deda)
|
||||
val md_theme_light_onSurfaceVariant = Color(0xFF534341)
|
||||
val md_theme_light_outline = Color(0xFF867370)
|
||||
val md_theme_light_inverseOnSurface = Color(0xFFfbeeec)
|
||||
val md_theme_light_inverseSurface = Color(0xFF362f2e)
|
||||
|
||||
val md_theme_dark_primary = Color(0xFFffb4a6)
|
||||
val md_theme_dark_onPrimary = Color(0xFF690000)
|
||||
val md_theme_dark_primaryContainer = Color(0xFF8a1a10)
|
||||
val md_theme_dark_onPrimaryContainer = Color(0xFFffdad3)
|
||||
val md_theme_dark_secondary = Color(0xFFe7bdb6)
|
||||
val md_theme_dark_onSecondary = Color(0xFF442925)
|
||||
val md_theme_dark_secondaryContainer = Color(0xFF5d3f3a)
|
||||
val md_theme_dark_onSecondaryContainer = Color(0xFFffdad3)
|
||||
val md_theme_dark_tertiary = Color(0xFFdec48c)
|
||||
val md_theme_dark_onTertiary = Color(0xFF3e2e04)
|
||||
val md_theme_dark_tertiaryContainer = Color(0xFF564418)
|
||||
val md_theme_dark_onTertiaryContainer = Color(0xFFfbdfa5)
|
||||
val md_theme_dark_error = Color(0xFFffb4a9)
|
||||
val md_theme_dark_errorContainer = Color(0xFF930006)
|
||||
val md_theme_dark_onError = Color(0xFF680003)
|
||||
val md_theme_dark_onErrorContainer = Color(0xFFffdad4)
|
||||
val md_theme_dark_background = Color(0xFF201a19)
|
||||
val md_theme_dark_onBackground = Color(0xFFede0de)
|
||||
val md_theme_dark_surface = Color(0xFF201a19)
|
||||
val md_theme_dark_onSurface = Color(0xFFede0de)
|
||||
val md_theme_dark_surfaceVariant = Color(0xFF534341)
|
||||
val md_theme_dark_onSurfaceVariant = Color(0xFFd8c2be)
|
||||
val md_theme_dark_outline = Color(0xFFa08c89)
|
||||
val md_theme_dark_inverseOnSurface = Color(0xFF201a19)
|
||||
val md_theme_dark_inverseSurface = Color(0xFFede0de)
|
||||
|
||||
val seed = Color(0xFF6c0000)
|
||||
val error = Color(0xFFba1b1b)
|
|
@ -0,0 +1,78 @@
|
|||
package dev.msfjarvis.claw.common.theme
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ProvidedValue
|
||||
|
||||
val LightThemeColors =
|
||||
lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
)
|
||||
|
||||
val DarkThemeColors =
|
||||
darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LobstersTheme(
|
||||
providedValues: Array<ProvidedValue<*>> = emptyArray(),
|
||||
colorScheme: ColorScheme,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
CompositionLocalProvider(*providedValues) {
|
||||
MaterialTheme(colorScheme = colorScheme, typography = AppTypography, content = content)
|
||||
}
|
||||
}
|
144
common/src/main/kotlin/dev/msfjarvis/claw/common/theme/Type.kt
Normal file
144
common/src/main/kotlin/dev/msfjarvis/claw/common/theme/Type.kt
Normal file
|
@ -0,0 +1,144 @@
|
|||
package dev.msfjarvis.claw.common.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.msfjarvis.claw.common.R
|
||||
|
||||
private val Manrope =
|
||||
FontFamily(
|
||||
Font(R.font.manrope_bold, FontWeight.Bold),
|
||||
Font(R.font.manrope_extrabold, FontWeight.ExtraBold),
|
||||
Font(R.font.manrope_extralight, FontWeight.ExtraLight),
|
||||
Font(R.font.manrope_light, FontWeight.Light),
|
||||
Font(R.font.manrope_medium, FontWeight.Medium),
|
||||
Font(R.font.manrope_regular, FontWeight.Normal),
|
||||
Font(R.font.manrope_semibold, FontWeight.SemiBold),
|
||||
)
|
||||
|
||||
val AppTypography =
|
||||
Typography(
|
||||
displayLarge =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = (-0.25).sp,
|
||||
),
|
||||
displayMedium =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
displaySmall =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
headlineLarge =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
headlineMedium =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
headlineSmall =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
titleLarge =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
titleMedium =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
titleSmall =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
labelLarge =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
bodyLarge =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
bodyMedium =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp,
|
||||
),
|
||||
bodySmall =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp,
|
||||
),
|
||||
labelMedium =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
labelSmall =
|
||||
TextStyle(
|
||||
fontFamily = Manrope,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
)
|
|
@ -0,0 +1,15 @@
|
|||
package dev.msfjarvis.claw.common.ui
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun Divider(modifier: Modifier = Modifier) {
|
||||
androidx.compose.material.Divider(
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = DividerAlpha),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
private const val DividerAlpha = 0.15f
|
|
@ -0,0 +1,23 @@
|
|||
package dev.msfjarvis.claw.common.ui
|
||||
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
||||
|
||||
@Composable
|
||||
fun ThemedRichText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides MaterialTheme.typography.bodyLarge,
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
|
||||
) {
|
||||
Material3RichText(modifier) { Markdown(text) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package dev.msfjarvis.claw.common.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun NetworkError(message: String, modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(text = message, style = MaterialTheme.typography.displayMedium)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package dev.msfjarvis.claw.common.ui
|
||||
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import coil.compose.AsyncImage
|
||||
|
||||
@Composable
|
||||
fun NetworkImage(
|
||||
url: String,
|
||||
contentDescription: String,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = url,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier.clip(CircleShape)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package dev.msfjarvis.claw.common.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun ProgressBar(modifier: Modifier) {
|
||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package dev.msfjarvis.claw.common.ui.decorations
|
||||
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SmallTopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun ClawAppBar(
|
||||
backgroundColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
navigationIcon: @Composable () -> Unit = {},
|
||||
title: @Composable () -> Unit = {},
|
||||
) {
|
||||
SmallTopAppBar(
|
||||
title = title,
|
||||
modifier = modifier,
|
||||
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = backgroundColor),
|
||||
navigationIcon = navigationIcon,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package dev.msfjarvis.claw.common.ui.decorations
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.ui.surfaceColorAtNavigationBarElevation
|
||||
import kotlinx.datetime.Month
|
||||
|
||||
@Composable
|
||||
fun MonthHeader(month: Month) {
|
||||
Box(
|
||||
Modifier.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtNavigationBarElevation())
|
||||
) {
|
||||
Text(
|
||||
text = month.name.lowercase().capitalize(Locale.current),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
27
common/src/main/kotlin/dev/msfjarvis/claw/common/ui/ext.kt
Normal file
27
common/src/main/kotlin/dev/msfjarvis/claw/common/ui/ext.kt
Normal file
|
@ -0,0 +1,27 @@
|
|||
package dev.msfjarvis.claw.common.ui
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.LocalAbsoluteTonalElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.ln
|
||||
|
||||
/**
|
||||
* Returns the [ColorScheme.surface] color with an alpha of the [ColorScheme.primary] color overlaid
|
||||
* on top of it. Computes the surface tonal color at different elevation levels e.g. surface1
|
||||
* through surface5.
|
||||
*
|
||||
* Stolen from AndroidX, keep in sync when upgrading Compose. This version is hard-coded to
|
||||
* replicate the logic used by the Material3 NavigationBar to determine its surface color.
|
||||
* https://github.com/androidx/androidx/blob/74d3510b608c3cc26b9cf9be8d15a6a6c26192c2/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt#L453-L466
|
||||
*/
|
||||
@Composable
|
||||
fun ColorScheme.surfaceColorAtNavigationBarElevation(): Color {
|
||||
// Absolute tonal elevation + NavigationBarTokens.ContainerElevation
|
||||
val elevation = LocalAbsoluteTonalElevation.current + 3.0.dp
|
||||
if (elevation == 0.dp) return surface
|
||||
val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
|
||||
return primary.copy(alpha = alpha).compositeOver(surface)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package dev.msfjarvis.claw.common.urllauncher
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import io.github.aakira.napier.Napier
|
||||
|
||||
class UrlLauncher(private val context: Context) : UriHandler {
|
||||
override fun openUri(uri: String) {
|
||||
val customTabsIntent =
|
||||
CustomTabsIntent.Builder()
|
||||
.setShareState(CustomTabsIntent.SHARE_STATE_ON)
|
||||
.setShowTitle(true)
|
||||
.setColorScheme(CustomTabsIntent.COLOR_SCHEME_DARK)
|
||||
.build()
|
||||
try {
|
||||
customTabsIntent.launchUrl(context, Uri.parse(uri))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
val error = "Failed to open URL: $uri"
|
||||
Napier.d(tag = "UrlLauncher") { error }
|
||||
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package dev.msfjarvis.claw.common.user
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.NetworkState
|
||||
import dev.msfjarvis.claw.common.NetworkState.Loading
|
||||
import dev.msfjarvis.claw.common.NetworkState.Success
|
||||
import dev.msfjarvis.claw.common.ui.NetworkError
|
||||
import dev.msfjarvis.claw.common.ui.NetworkImage
|
||||
import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||
import dev.msfjarvis.claw.common.ui.ThemedRichText
|
||||
import dev.msfjarvis.claw.model.User
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
fun UserProfile(
|
||||
username: String,
|
||||
getProfile: suspend (username: String) -> User,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val user by produceState<NetworkState>(Loading) { value = Success(getProfile(username)) }
|
||||
when (user) {
|
||||
is Success<*> -> {
|
||||
UserProfileInternal((user as Success<User>).data)
|
||||
}
|
||||
is NetworkState.Error -> {
|
||||
NetworkError((user as NetworkState.Error).message)
|
||||
}
|
||||
Loading -> ProgressBar(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserProfileInternal(
|
||||
user: User,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(modifier = modifier) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
NetworkImage(
|
||||
url = "https://lobste.rs/${user.avatarUrl}",
|
||||
contentDescription = "Avatar of ${user.username}",
|
||||
modifier = Modifier.requiredSize(120.dp).clip(CircleShape),
|
||||
)
|
||||
Text(
|
||||
text = user.username,
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
)
|
||||
ThemedRichText(
|
||||
text = user.about,
|
||||
)
|
||||
ThemedRichText(
|
||||
text = "Invited by [${user.invitedBy}](https://lobste.rs/u/${user.invitedBy})",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
3
common/src/main/kotlin/dev/msfjarvis/claw/common/util.kt
Normal file
3
common/src/main/kotlin/dev/msfjarvis/claw/common/util.kt
Normal file
|
@ -0,0 +1,3 @@
|
|||
package dev.msfjarvis.claw.common
|
||||
|
||||
fun <T> unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE, initializer)
|
BIN
common/src/main/res/font/manrope_bold.ttf
Normal file
BIN
common/src/main/res/font/manrope_bold.ttf
Normal file
Binary file not shown.
BIN
common/src/main/res/font/manrope_extrabold.ttf
Normal file
BIN
common/src/main/res/font/manrope_extrabold.ttf
Normal file
Binary file not shown.
BIN
common/src/main/res/font/manrope_extralight.ttf
Normal file
BIN
common/src/main/res/font/manrope_extralight.ttf
Normal file
Binary file not shown.
BIN
common/src/main/res/font/manrope_light.ttf
Normal file
BIN
common/src/main/res/font/manrope_light.ttf
Normal file
Binary file not shown.
BIN
common/src/main/res/font/manrope_medium.ttf
Normal file
BIN
common/src/main/res/font/manrope_medium.ttf
Normal file
Binary file not shown.
BIN
common/src/main/res/font/manrope_regular.ttf
Normal file
BIN
common/src/main/res/font/manrope_regular.ttf
Normal file
Binary file not shown.
BIN
common/src/main/res/font/manrope_semibold.ttf
Normal file
BIN
common/src/main/res/font/manrope_semibold.ttf
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue