mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 19:57:04 +05:30
Merge pull request #258 from msfjarvis/comments-screen
This commit is contained in:
commit
aa1bd18411
16 changed files with 318 additions and 87 deletions
|
@ -20,7 +20,10 @@ dependencies {
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.androidx.coreKtx)
|
implementation(libs.androidx.coreKtx)
|
||||||
implementation(libs.androidx.lifecycle.compose)
|
implementation(libs.androidx.lifecycle.compose)
|
||||||
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.androidx.paging.compose)
|
implementation(libs.androidx.paging.compose)
|
||||||
|
implementation(libs.multiplatform.markdown.android)
|
||||||
|
implementation(libs.copydown)
|
||||||
implementation(libs.dagger.hilt.android)
|
implementation(libs.dagger.hilt.android)
|
||||||
implementation(libs.sqldelight.extensions.coroutines)
|
implementation(libs.sqldelight.extensions.coroutines)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
package dev.msfjarvis.claw.android.ext
|
|
||||||
|
|
||||||
import dev.msfjarvis.claw.database.local.SavedPost
|
|
||||||
import dev.msfjarvis.claw.model.LobstersPost
|
|
||||||
|
|
||||||
/** Convert a [LobstersPost] object returned by the API into a [SavedPost] for persistence. */
|
|
||||||
fun LobstersPost.toDbModel(): SavedPost {
|
|
||||||
return SavedPost(
|
|
||||||
shortId = shortId,
|
|
||||||
title = title,
|
|
||||||
url = url,
|
|
||||||
createdAt = createdAt,
|
|
||||||
commentsUrl = commentsUrl,
|
|
||||||
submitterName = submitter.username,
|
|
||||||
submitterAvatarUrl = submitter.avatarUrl,
|
|
||||||
tags = tags,
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
package dev.msfjarvis.claw.android.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||||
|
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||||
|
import dev.msfjarvis.claw.database.local.SavedPost
|
||||||
|
import dev.msfjarvis.claw.model.LobstersPost
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HottestPosts(
|
||||||
|
items: LazyPagingItems<LobstersPost>,
|
||||||
|
listState: LazyListState,
|
||||||
|
isPostSaved: suspend (SavedPost) -> Boolean,
|
||||||
|
toggleSave: suspend (SavedPost) -> Unit,
|
||||||
|
reloadPosts: () -> Unit,
|
||||||
|
launchUrl: (String) -> Unit,
|
||||||
|
viewComments: (String) -> Unit,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val isRefreshing = items.loadState.refresh == LoadState.Loading
|
||||||
|
SwipeRefresh(
|
||||||
|
state = rememberSwipeRefreshState(isRefreshing),
|
||||||
|
onRefresh = reloadPosts,
|
||||||
|
) {
|
||||||
|
if (items.itemCount == 0) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize())
|
||||||
|
} else {
|
||||||
|
NetworkPosts(
|
||||||
|
items = items,
|
||||||
|
launchUrl = launchUrl,
|
||||||
|
listState = listState,
|
||||||
|
isSaved = isPostSaved,
|
||||||
|
viewComments = viewComments,
|
||||||
|
toggleSave = { coroutineScope.launch { toggleSave(it) } },
|
||||||
|
modifier = Modifier.padding(top = 16.dp).then(modifier),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,6 @@ package dev.msfjarvis.claw.android.ui
|
||||||
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Scaffold
|
import androidx.compose.material.Scaffold
|
||||||
|
@ -15,7 +12,6 @@ import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
@ -23,20 +19,21 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.paging.LoadState
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import com.google.accompanist.insets.ProvideWindowInsets
|
import com.google.accompanist.insets.ProvideWindowInsets
|
||||||
import com.google.accompanist.insets.navigationBarsPadding
|
import com.google.accompanist.insets.navigationBarsPadding
|
||||||
import com.google.accompanist.insets.statusBarsPadding
|
import com.google.accompanist.insets.statusBarsPadding
|
||||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
|
||||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
|
import com.mikepenz.markdown.Markdown
|
||||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||||
|
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||||
import kotlinx.coroutines.launch
|
import io.github.furstenheim.CopyDown
|
||||||
|
|
||||||
private const val ScrollDelta = 50
|
private const val ScrollDelta = 50
|
||||||
|
|
||||||
|
@ -46,10 +43,11 @@ fun LobstersApp(
|
||||||
viewModel: ClawViewModel = viewModel(),
|
viewModel: ClawViewModel = viewModel(),
|
||||||
urlLauncher: UrlLauncher,
|
urlLauncher: UrlLauncher,
|
||||||
) {
|
) {
|
||||||
|
val copydown = remember { CopyDown() }
|
||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
val scaffoldState = rememberScaffoldState()
|
val scaffoldState = rememberScaffoldState()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val navController = rememberNavController()
|
||||||
var isFabVisible by remember { mutableStateOf(true) }
|
var isFabVisible by remember { mutableStateOf(true) }
|
||||||
val nestedScrollConnection = remember {
|
val nestedScrollConnection = remember {
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
|
@ -77,32 +75,40 @@ fun LobstersApp(
|
||||||
systemUiController.setNavigationBarColor(color = Color.Transparent)
|
systemUiController.setNavigationBarColor(color = Color.Transparent)
|
||||||
}
|
}
|
||||||
val items = viewModel.pagerFlow.collectAsLazyPagingItems()
|
val items = viewModel.pagerFlow.collectAsLazyPagingItems()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
scaffoldState = scaffoldState,
|
scaffoldState = scaffoldState,
|
||||||
topBar = { ClawAppBar(modifier = Modifier.statusBarsPadding()) },
|
topBar = { ClawAppBar(modifier = Modifier.statusBarsPadding()) },
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
ClawFab(
|
ClawFab(
|
||||||
isFabVisible = isFabVisible,
|
isFabVisible = isFabVisible && navController.currentDestination?.route == "hottest",
|
||||||
listState = listState,
|
listState = listState,
|
||||||
modifier = Modifier.navigationBarsPadding(),
|
modifier = Modifier.navigationBarsPadding(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) { paddingValues ->
|
||||||
val isRefreshing = items.loadState.refresh == LoadState.Loading
|
NavHost(navController, startDestination = "hottest") {
|
||||||
SwipeRefresh(
|
composable("hottest") {
|
||||||
state = rememberSwipeRefreshState(isRefreshing),
|
HottestPosts(
|
||||||
onRefresh = viewModel::reloadPosts,
|
items,
|
||||||
) {
|
listState,
|
||||||
if (items.itemCount == 0) {
|
viewModel::isPostSaved,
|
||||||
Box(modifier = Modifier.fillMaxSize())
|
viewModel::toggleSave,
|
||||||
} else {
|
viewModel::reloadPosts,
|
||||||
NetworkPosts(
|
urlLauncher::launch,
|
||||||
items = items,
|
{ navController.navigate("comments/$it") },
|
||||||
launchUrl = urlLauncher::launch,
|
Modifier.nestedScroll(nestedScrollConnection),
|
||||||
listState = listState,
|
)
|
||||||
isSaved = viewModel::isPostSaved,
|
}
|
||||||
toggleSave = { coroutineScope.launch { viewModel.toggleSave(it) } },
|
composable("comments/{postId}") { backStackEntry ->
|
||||||
modifier = Modifier.padding(top = 16.dp).nestedScroll(nestedScrollConnection),
|
CommentsPage(
|
||||||
|
postId = requireNotNull(backStackEntry.arguments?.getString("postId")),
|
||||||
|
getDetails = viewModel::getPostComments,
|
||||||
|
renderMarkdown = { source, modifier ->
|
||||||
|
val markdown = copydown.convert(source)
|
||||||
|
Markdown(markdown, modifier = modifier)
|
||||||
|
},
|
||||||
|
paddingValues = paddingValues,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,8 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.items
|
import androidx.paging.compose.items
|
||||||
import dev.msfjarvis.claw.android.ext.toDbModel
|
|
||||||
import dev.msfjarvis.claw.common.posts.LobstersCard
|
import dev.msfjarvis.claw.common.posts.LobstersCard
|
||||||
|
import dev.msfjarvis.claw.common.posts.toDbModel
|
||||||
import dev.msfjarvis.claw.database.local.SavedPost
|
import dev.msfjarvis.claw.database.local.SavedPost
|
||||||
import dev.msfjarvis.claw.model.LobstersPost
|
import dev.msfjarvis.claw.model.LobstersPost
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -26,6 +26,7 @@ fun NetworkPosts(
|
||||||
launchUrl: (String) -> Unit,
|
launchUrl: (String) -> Unit,
|
||||||
isSaved: suspend (SavedPost) -> Boolean,
|
isSaved: suspend (SavedPost) -> Boolean,
|
||||||
toggleSave: (SavedPost) -> Unit,
|
toggleSave: (SavedPost) -> Unit,
|
||||||
|
viewComments: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
@ -42,7 +43,7 @@ fun NetworkPosts(
|
||||||
post = dbModel,
|
post = dbModel,
|
||||||
isSaved = saved,
|
isSaved = saved,
|
||||||
viewPost = { launchUrl(item.url.ifEmpty { item.commentsUrl }) },
|
viewPost = { launchUrl(item.url.ifEmpty { item.commentsUrl }) },
|
||||||
viewComments = { launchUrl(item.commentsUrl) },
|
viewComments = { viewComments(item.shortId) },
|
||||||
toggleSave = { toggleSave(dbModel) },
|
toggleSave = { toggleSave(dbModel) },
|
||||||
modifier = Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
|
modifier = Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,19 +7,22 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource
|
import dev.msfjarvis.claw.android.paging.LobstersPagingSource
|
||||||
import dev.msfjarvis.claw.api.LobstersApi
|
import dev.msfjarvis.claw.api.LobstersApi
|
||||||
import dev.msfjarvis.claw.database.local.SavedPost
|
import dev.msfjarvis.claw.database.local.SavedPost
|
||||||
|
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.last
|
import kotlinx.coroutines.flow.last
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ClawViewModel
|
class ClawViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
api: LobstersApi,
|
private val api: LobstersApi,
|
||||||
private val repository: SavedPostsRepository,
|
private val repository: SavedPostsRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
var lastPagingSource: LobstersPagingSource? = null
|
var lastPagingSource: LobstersPagingSource? = null
|
||||||
|
@ -46,6 +49,11 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getPostComments(postId: String): LobstersPostDetails =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
return@withContext api.getPostDetails(postId)
|
||||||
|
}
|
||||||
|
|
||||||
fun reloadPosts() {
|
fun reloadPosts() {
|
||||||
lastPagingSource?.invalidate()
|
lastPagingSource?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ buildscript {
|
||||||
classpath("com.android.tools:r8:3.1.17-dev")
|
classpath("com.android.tools:r8:3.1.17-dev")
|
||||||
classpath(kotlin("gradle-plugin", version = kotlinVersion))
|
classpath(kotlin("gradle-plugin", version = kotlinVersion))
|
||||||
classpath(kotlin("serialization", version = kotlinVersion))
|
classpath(kotlin("serialization", version = kotlinVersion))
|
||||||
classpath("com.android.tools.build:gradle:7.1.0-alpha12")
|
classpath("com.android.tools.build:gradle:7.1.0-alpha13")
|
||||||
classpath("com.diffplug.spotless:spotless-plugin-gradle:5.15.0")
|
classpath("com.diffplug.spotless:spotless-plugin-gradle:5.15.0")
|
||||||
classpath("com.google.dagger:hilt-android-gradle-plugin:2.39")
|
classpath("com.google.dagger:hilt-android-gradle-plugin:2.39")
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ kotlin {
|
||||||
api(compose.foundation)
|
api(compose.foundation)
|
||||||
api(compose.material)
|
api(compose.material)
|
||||||
api(projects.database)
|
api(projects.database)
|
||||||
|
api(projects.model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sourceSets["androidMain"].apply {
|
sourceSets["androidMain"].apply {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.msfjarvis.lobsters.ui.comments
|
||||||
|
|
||||||
|
sealed class NetworkState {
|
||||||
|
class Success<T>(val data: T) : NetworkState()
|
||||||
|
class Error(val message: String) : NetworkState()
|
||||||
|
object Loading : NetworkState()
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package dev.msfjarvis.claw.common.comments
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.Divider
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.msfjarvis.claw.common.posts.PostDetails
|
||||||
|
import dev.msfjarvis.claw.common.posts.SubmitterName
|
||||||
|
import dev.msfjarvis.claw.common.posts.toDbModel
|
||||||
|
import dev.msfjarvis.claw.model.Comment
|
||||||
|
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CommentsHeader(
|
||||||
|
postDetails: LobstersPostDetails,
|
||||||
|
) {
|
||||||
|
Surface {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp).fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
PostDetails(
|
||||||
|
post = postDetails.toDbModel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CommentEntry(
|
||||||
|
comment: Comment,
|
||||||
|
renderMarkdown: @Composable (comment: String, modifier: Modifier) -> Unit,
|
||||||
|
) {
|
||||||
|
val indentLevel = comment.indentLevel.toInt() - 1
|
||||||
|
|
||||||
|
Divider(color = Color.Gray.copy(0.4f))
|
||||||
|
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||||
|
CommentTreeColors(indentLevel = indentLevel)
|
||||||
|
Column(modifier = Modifier.padding(start = 12.dp, end = 8.dp, top = 4.dp, bottom = 4.dp)) {
|
||||||
|
SubmitterName(
|
||||||
|
text = "Submitted by ${comment.user.username}",
|
||||||
|
avatarUrl = "https://lobste.rs/${comment.user.avatarUrl}",
|
||||||
|
contentDescription = "Submitted by ${comment.user.username}",
|
||||||
|
)
|
||||||
|
renderMarkdown(comment.comment, Modifier.padding(top = 8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CommentTreeColors(
|
||||||
|
indentLevel: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
for (level in 1..indentLevel) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier.padding(start = (12 + ((level - 1) * 10)).dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(1.dp)
|
||||||
|
.background(CommentTreeColor[level])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,7 @@ object CommentTreeColor {
|
||||||
Color(0xFFFF0097),
|
Color(0xFFFF0097),
|
||||||
Color(0xFF1E7145),
|
Color(0xFF1E7145),
|
||||||
)
|
)
|
||||||
private val size = colors.size
|
val size = colors.size
|
||||||
|
|
||||||
operator fun get(idx: Int): Color = colors[idx % size]
|
operator fun get(idx: Int): Color = colors[idx % size]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
package dev.msfjarvis.claw.common.comments
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.material.Divider
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||||
|
import dev.msfjarvis.lobsters.ui.comments.NetworkState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CommentsPageInternal(
|
||||||
|
details: LobstersPostDetails,
|
||||||
|
renderMarkdown: @Composable (comment: String, modifier: Modifier) -> Unit,
|
||||||
|
bottomPadding: Dp,
|
||||||
|
) {
|
||||||
|
LazyColumn(Modifier.padding(bottom = bottomPadding)) {
|
||||||
|
item { CommentsHeader(postDetails = details) }
|
||||||
|
|
||||||
|
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||||
|
|
||||||
|
items(details.comments) { item -> CommentEntry(item, renderMarkdown) }
|
||||||
|
|
||||||
|
item { Divider(color = Color.Gray.copy(0.4f)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
@Composable
|
||||||
|
fun CommentsPage(
|
||||||
|
postId: String,
|
||||||
|
getDetails: suspend (String) -> LobstersPostDetails,
|
||||||
|
renderMarkdown: @Composable (comment: String, modifier: Modifier) -> Unit,
|
||||||
|
paddingValues: PaddingValues,
|
||||||
|
) {
|
||||||
|
var postDetails: NetworkState by remember { mutableStateOf(NetworkState.Loading) }
|
||||||
|
|
||||||
|
LaunchedEffect(postId) { postDetails = NetworkState.Success(getDetails(postId)) }
|
||||||
|
|
||||||
|
when (postDetails) {
|
||||||
|
is NetworkState.Success<*> -> {
|
||||||
|
CommentsPageInternal(
|
||||||
|
(postDetails as NetworkState.Success<LobstersPostDetails>).data,
|
||||||
|
renderMarkdown,
|
||||||
|
paddingValues.calculateBottomPadding(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is NetworkState.Error -> TODO("Handle no network scenario")
|
||||||
|
NetworkState.Loading -> ProgressBar(paddingValues.calculateBottomPadding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProgressBar(bottomPadding: Dp) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(bottom = bottomPadding).fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) { CircularProgressIndicator(color = MaterialTheme.colors.secondary) }
|
||||||
|
}
|
|
@ -37,18 +37,6 @@ import dev.msfjarvis.claw.common.theme.titleColor
|
||||||
import dev.msfjarvis.claw.common.ui.NetworkImage
|
import dev.msfjarvis.claw.common.ui.NetworkImage
|
||||||
import dev.msfjarvis.claw.database.local.SavedPost
|
import dev.msfjarvis.claw.database.local.SavedPost
|
||||||
|
|
||||||
val TEST_POST =
|
|
||||||
SavedPost(
|
|
||||||
shortId = "zqyydb",
|
|
||||||
title = "k2k20 hackathon report: Bob Beck on LibreSSL progress",
|
|
||||||
url = "https://undeadly.org/cgi?action=article;sid=20200921105847",
|
|
||||||
createdAt = "2020-09-21T07:11:14.000-05:00",
|
|
||||||
commentsUrl = "https://lobste.rs/s/zqyydb/k2k20_hackathon_report_bob_beck_on",
|
|
||||||
submitterName = "Vigdis",
|
|
||||||
submitterAvatarUrl = "https://avatars.githubusercontent.com/u/13348378?v=4",
|
|
||||||
tags = listOf("openbsd", "linux", "containers", "hack the planet", "no thanks"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
fun LobstersCard(
|
fun LobstersCard(
|
||||||
|
@ -67,16 +55,8 @@ fun LobstersCard(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp).fillMaxWidth(),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp).fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
PostTitle(
|
PostDetails(
|
||||||
title = post.title,
|
post = post,
|
||||||
)
|
|
||||||
TagRow(
|
|
||||||
tags = post.tags,
|
|
||||||
)
|
|
||||||
SubmitterName(
|
|
||||||
text = "Submitted by ${post.submitterName}",
|
|
||||||
avatarUrl = "https://lobste.rs/${post.submitterAvatarUrl}",
|
|
||||||
contentDescription = "Submitted by ${post.submitterName}",
|
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||||
|
@ -98,6 +78,23 @@ fun LobstersCard(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PostDetails(
|
||||||
|
post: SavedPost,
|
||||||
|
) {
|
||||||
|
PostTitle(
|
||||||
|
title = post.title,
|
||||||
|
)
|
||||||
|
TagRow(
|
||||||
|
tags = post.tags,
|
||||||
|
)
|
||||||
|
SubmitterName(
|
||||||
|
text = "Submitted by ${post.submitterName}",
|
||||||
|
avatarUrl = "https://lobste.rs/${post.submitterAvatarUrl}",
|
||||||
|
contentDescription = "Submitted by ${post.submitterName}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PostTitle(
|
fun PostTitle(
|
||||||
title: String,
|
title: String,
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
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,
|
||||||
|
commentsUrl = commentsUrl,
|
||||||
|
submitterName = submitter.username,
|
||||||
|
submitterAvatarUrl = submitter.avatarUrl,
|
||||||
|
tags = tags,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LobstersPostDetails.toDbModel(): SavedPost {
|
||||||
|
return SavedPost(
|
||||||
|
shortId = shortId,
|
||||||
|
title = title,
|
||||||
|
url = url,
|
||||||
|
createdAt = createdAt,
|
||||||
|
commentsUrl = commentsUrl,
|
||||||
|
submitterName = submitter.username,
|
||||||
|
submitterAvatarUrl = submitter.avatarUrl,
|
||||||
|
tags = tags,
|
||||||
|
)
|
||||||
|
}
|
|
@ -14,27 +14,13 @@ import androidx.compose.ui.window.WindowPosition
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import androidx.compose.ui.window.rememberWindowState
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
import dev.msfjarvis.claw.common.posts.LobstersCard
|
import dev.msfjarvis.claw.common.posts.LobstersCard
|
||||||
|
import dev.msfjarvis.claw.common.posts.toDbModel
|
||||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||||
import dev.msfjarvis.claw.database.local.SavedPost
|
|
||||||
import dev.msfjarvis.claw.model.LobstersPost
|
|
||||||
import org.pushingpixels.aurora.component.AuroraVerticalScrollbar
|
import org.pushingpixels.aurora.component.AuroraVerticalScrollbar
|
||||||
import org.pushingpixels.aurora.skin.ceruleanSkin
|
import org.pushingpixels.aurora.skin.ceruleanSkin
|
||||||
import org.pushingpixels.aurora.window.AuroraWindow
|
import org.pushingpixels.aurora.window.AuroraWindow
|
||||||
|
|
||||||
fun LobstersPost.toDbModel(): SavedPost {
|
|
||||||
return SavedPost(
|
|
||||||
shortId = shortId,
|
|
||||||
title = title,
|
|
||||||
url = url,
|
|
||||||
createdAt = createdAt,
|
|
||||||
commentsUrl = commentsUrl,
|
|
||||||
submitterName = submitter.username,
|
|
||||||
submitterAvatarUrl = submitter.avatarUrl,
|
|
||||||
tags = tags,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
val paging = Paging(rememberCoroutineScope())
|
val paging = Paging(rememberCoroutineScope())
|
||||||
val items = paging.pagingData.collectAsLazyPagingItems()
|
val items = paging.pagingData.collectAsLazyPagingItems()
|
||||||
|
|
|
@ -23,6 +23,7 @@ androidx-appcompat = "androidx.appcompat:appcompat:1.4.0-beta01"
|
||||||
androidx-browser = "androidx.browser:browser:1.4.0-beta01"
|
androidx-browser = "androidx.browser:browser:1.4.0-beta01"
|
||||||
androidx-coreKtx = "androidx.core:core-ktx:1.7.0-beta02"
|
androidx-coreKtx = "androidx.core:core-ktx:1.7.0-beta02"
|
||||||
androidx-lifecycle-compose = "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0-rc01"
|
androidx-lifecycle-compose = "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0-rc01"
|
||||||
|
androidx-navigation-compose = "androidx.navigation:navigation-compose:2.4.0-alpha10"
|
||||||
androidx-paging-compose = "androidx.paging:paging-compose:1.0.0-alpha13"
|
androidx-paging-compose = "androidx.paging:paging-compose:1.0.0-alpha13"
|
||||||
|
|
||||||
aurora-component = { module = "org.pushing-pixels:aurora-component", version.ref = "aurora" }
|
aurora-component = { module = "org.pushing-pixels:aurora-component", version.ref = "aurora" }
|
||||||
|
@ -36,6 +37,9 @@ dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref =
|
||||||
dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
|
dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
|
||||||
dagger-hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" }
|
dagger-hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" }
|
||||||
|
|
||||||
|
multiplatform-markdown-android = "com.mikepenz:multiplatform-markdown-renderer-android:0.0.1"
|
||||||
|
copydown = "io.github.furstenheim:copy_down:1.0"
|
||||||
|
|
||||||
multiplatform-paging = "dev.msfjarvis.paging:multiplatform-paging:0.4.5-SNAPSHOT"
|
multiplatform-paging = "dev.msfjarvis.paging:multiplatform-paging:0.4.5-SNAPSHOT"
|
||||||
|
|
||||||
retrofit-lib = "com.squareup.retrofit2:retrofit:2.9.0"
|
retrofit-lib = "com.squareup.retrofit2:retrofit:2.9.0"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue