feat(posts): improve post view

This commit is contained in:
Diego Beraldin 2023-07-26 22:43:30 +02:00
parent 51a3d2b054
commit 4b80c78e30
38 changed files with 617 additions and 102 deletions

View File

@ -12,4 +12,8 @@ val coreApiModule = module {
val provider: ServiceProvider = get()
provider.postService
}
single {
val provider: ServiceProvider = get()
provider.communityService
}
}

View File

@ -0,0 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class BlockCommunityForm(
@SerialName("community_id") val communityId: CommunityId,
@SerialName("block") val block: Boolean,
@SerialName("auth") val auth: String,
)

View File

@ -0,0 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class BlockCommunityResponse(
@SerialName("community_view") val communityView: CommunityView,
@SerialName("blocked") val blocked: Boolean,
)

View File

@ -0,0 +1,21 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Comment(
@SerialName("id") val id: CommentId,
@SerialName("creator_id") val creatorId: PersonId,
@SerialName("post_id") val postId: PostId,
@SerialName("content") val content: String,
@SerialName("removed") val removed: Boolean,
@SerialName("published") val published: String,
@SerialName("updated") val updated: String? = null,
@SerialName("deleted") val deleted: Boolean,
@SerialName("ap_id") val apId: String,
@SerialName("local") val local: Boolean,
@SerialName("path") val path: String,
@SerialName("distinguished") val distinguished: Boolean,
@SerialName("language_id") val languageId: LanguageId,
)

View File

@ -0,0 +1,16 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CommentAggregates(
@SerialName("id") val id: Int,
@SerialName("comment_id") val commentId: CommentId,
@SerialName("score") val score: Int,
@SerialName("upvotes") val upvotes: Int,
@SerialName("downvotes") val downvotes: Int,
@SerialName("published") val published: String,
@SerialName("child_count") val childCount: Int,
@SerialName("hot_rank") val hotRank: Int,
)

View File

@ -0,0 +1,18 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CommentView(
@SerialName("comment") val comment: Comment,
@SerialName("creator") val creator: Person,
@SerialName("post") val post: Post,
@SerialName("community") val community: Community,
@SerialName("counts") val counts: CommentAggregates,
@SerialName("creator_banned_from_community") val creatorBannedFromCommunity: Boolean,
@SerialName("subscribed") val subscribed: SubscribedType,
@SerialName("saved") val saved: Boolean,
@SerialName("creator_blocked") val creatorBlocked: Boolean,
@SerialName("my_vote") val myVote: Int? = null,
)

View File

@ -0,0 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CommunityResponse(
@SerialName("community_view") val communityView: CommunityView,
@SerialName("discussion_languages") val discussionLanguages: List<LanguageId>,
)

View File

@ -6,3 +6,4 @@ typealias CommunityId = Int
typealias LanguageId = Int
typealias InstanceId = Int
typealias CommentId = Int
typealias SiteId = Int

View File

@ -0,0 +1,16 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreatePostForm(
@SerialName("name") val name: String,
@SerialName("community_id") val communityId: CommunityId,
@SerialName("url") val url: String? = null,
@SerialName("body") val body: String? = null,
@SerialName("honeypot") val honeypot: String? = null,
@SerialName("nsfw") val nsfw: Boolean? = null,
@SerialName("language_id") val languageId: LanguageId? = null,
@SerialName("auth") val auth: String,
)

View File

@ -0,0 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class DeletePostForm(
@SerialName("post_id") val postId: PostId,
@SerialName("deleted") val deleted: Boolean,
@SerialName("auth") val auth: String,
)

View File

@ -0,0 +1,15 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class EditPostForm(
@SerialName("post_id") val postId: PostId,
@SerialName("name") val name: String? = null,
@SerialName("url") val url: String? = null,
@SerialName("body") val body: String? = null,
@SerialName("nsfw") val nsfw: Boolean? = null,
@SerialName("language_id") val languageId: LanguageId? = null,
@SerialName("auth") val auth: String,
)

View File

@ -0,0 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class FollowCommunityForm(
@SerialName("community_id") val communityId: CommunityId,
@SerialName("follow") val follow: Boolean,
@SerialName("auth") val auth: String,
)

View File

@ -0,0 +1,13 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetCommunityResponse(
@SerialName("community_view") val communityView: CommunityView,
@SerialName("site") val site: Site? = null,
@SerialName("moderators") val moderators: List<CommunityModeratorView>,
@SerialName("discussion_languages") val discussionLanguages: List<LanguageId>,
)

View File

@ -0,0 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetPersonDetailsResponse(
@SerialName("person_view") val personView: PersonView,
@SerialName("comments") val comments: List<CommentView>,
@SerialName("posts") val posts: List<PostView>,
@SerialName("moderates") val moderates: List<CommunityModeratorView>,
)

View File

@ -0,0 +1,14 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PersonAggregates(
@SerialName("id") val id: Int,
@SerialName("person_id") val personId: PersonId,
@SerialName("post_count") val postCount: Int,
@SerialName("post_score") val postScore: Int,
@SerialName("comment_count") val commentCount: Int,
@SerialName("comment_score") val commentScore: Int,
)

View File

@ -0,0 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PersonView(
@SerialName("person") val person: Person,
@SerialName("counts") val counts: PersonAggregates,
)

View File

@ -0,0 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PostResponse(
@SerialName("post_view")
val postView: PostView,
)

View File

@ -0,0 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SavePostForm(
@SerialName("post_id") val postId: PostId,
@SerialName("save") val save: Boolean,
@SerialName("auth") val auth: String,
)

View File

@ -0,0 +1,22 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Site(
@SerialName("id") val id: SiteId,
@SerialName("name") val name: String,
@SerialName("sidebar") val sidebar: String? = null,
@SerialName("published") val published: String,
@SerialName("updated") val updated: String? = null,
@SerialName("icon") val icon: String? = null,
@SerialName("banner") val banner: String? = null,
@SerialName("description") val description: String? = null,
@SerialName("actor_id") val actorId: String,
@SerialName("last_refreshed_at") val lastRefreshedAt: String,
@SerialName("inbox_url") val inboxUrl: String,
@SerialName("private_key") val privateKey: String? = null,
@SerialName("public_key") val publicKey: String,
@SerialName("instance_id") val instanceId: InstanceId,
)

View File

@ -1,5 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.provider
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.CommunityService
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.PostService
import de.jensklingenberg.ktorfit.Ktorfit
import io.ktor.client.HttpClient
@ -12,7 +13,7 @@ import kotlinx.serialization.json.Json
internal class DefaultServiceProvider : ServiceProvider {
companion object {
private const val DEFAULT_INSTANCE = "enterprise.lemmy.ml"
private const val DEFAULT_INSTANCE = "lemmy.world"
private const val VERSION = "v3"
}
@ -22,6 +23,9 @@ internal class DefaultServiceProvider : ServiceProvider {
override lateinit var postService: PostService
private set
override lateinit var communityService: CommunityService
private set
private val baseUrl: String get() = "https://$currentInstance/api/$VERSION/"
private val client = HttpClient {
install(Logging) {
@ -49,5 +53,6 @@ internal class DefaultServiceProvider : ServiceProvider {
.httpClient(client)
.build()
postService = ktorfit.create()
communityService = ktorfit.create()
}
}

View File

@ -1,11 +1,13 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.provider
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.CommunityService
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.PostService
interface ServiceProvider {
val currentInstance: String
val postService: PostService
val communityService: CommunityService
fun changeInstance(value: String)
}

View File

@ -0,0 +1,29 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.service
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.BlockCommunityForm
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.BlockCommunityResponse
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.CommunityResponse
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.FollowCommunityForm
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.GetCommunityResponse
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Query
interface CommunityService {
@GET("community")
suspend fun getCommunity(
@Query("auth") auth: String? = null,
@Query("id") id: Int? = null,
@Query("name") name: String? = null,
): Response<GetCommunityResponse>
@POST("community/follow")
suspend fun followCommunity(@Body form: FollowCommunityForm): Response<CommunityResponse>
@POST("community/block")
suspend fun blockCommunity(@Body form: BlockCommunityForm): Response<BlockCommunityResponse>
}

View File

@ -1,11 +1,19 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.service
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.CreatePostForm
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.DeletePostForm
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.EditPostForm
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.GetPostResponse
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.GetPostsResponse
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.ListingType
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.PostResponse
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.SavePostForm
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.SortType
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.PUT
import de.jensklingenberg.ktorfit.http.Query
interface PostService {
@ -28,4 +36,17 @@ interface PostService {
@Query("id") id: Int? = null,
@Query("comment_id") commentId: Int? = null,
): Response<GetPostResponse>
@PUT("post/save")
suspend fun savePost(@Body form: SavePostForm): Response<PostResponse>
@POST("post")
suspend fun createPost(@Body form: CreatePostForm): Response<PostResponse>
@PUT("post")
suspend fun editPost(@Body form: EditPostForm): Response<PostResponse>
@POST("post/delete")
suspend fun deletePost(@Body form: DeletePostForm): Response<PostResponse>
}

View File

@ -0,0 +1,22 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.service
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.GetPersonDetailsResponse
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.SortType
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Query
interface UserService {
@GET("user")
suspend fun getPersonDetails(
@Query("auth") auth: String? = null,
@Query("community_id") communityId: Int? = null,
@Query("person_id") personId: Int? = null,
@Query("page") page: Int? = null,
@Query("limit") limit: Int? = null,
@Query("sort") sort: SortType,
@Query("username") username: String? = null,
@Query("saved_only") savedOnly: Boolean? = null,
): Response<GetPersonDetailsResponse>
}

View File

@ -0,0 +1,8 @@
package com.github.diegoberaldin.raccoonforlemmy.data
data class CommunityModel(
val id: Int = 0,
val name: String = "",
val instance: String = "",
val icon: String? = null,
)

View File

@ -1,6 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.data
data class PostModel(
val id: Int = 0,
val title: String = "",
val text: String = "",
val score: Int = 0,
val comments: Int = 0,
val thumbnailUrl: String? = null,
val community: CommunityModel? = null,
)

View File

@ -0,0 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.data
data class UserModel(
val name: String = "",
)

View File

@ -0,0 +1,20 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_post.repository
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.CommunityView
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.CommunityService
import com.github.diegoberaldin.raccoonforlemmy.data.CommunityModel
class CommunityRepository(
private val communityService: CommunityService,
) {
suspend fun getCommunity(id: Int): CommunityModel? {
val response = communityService.getCommunity(id = id).body()
return response?.communityView?.toModel()
}
}
private fun CommunityView.toModel() = CommunityModel(
name = community.name,
icon = community.icon,
)

View File

@ -2,6 +2,7 @@ package com.github.diegoberaldin.raccoonforlemmy.domain_post.repository
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.PostView
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.PostService
import com.github.diegoberaldin.raccoonforlemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.data.SortType
@ -34,8 +35,13 @@ class PostsRepository(
}
private fun PostView.toModel() = PostModel(
title = this.post.name,
text = this.post.body.orEmpty(),
id = post.id,
title = post.name,
text = post.body.orEmpty(),
score = counts.score,
comments = counts.comments,
thumbnailUrl = post.thumbnailUrl.orEmpty(),
community = CommunityModel(id = post.communityId)
)
private fun ListingType.toDto() = when (this) {

View File

@ -4,6 +4,7 @@ import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val postsRepositoryModule = module {
singleOf(::PostsRepository)
singleOf(::ApiConfigurationRepository)
singleOf(::PostsRepository)
singleOf(::CommunityRepository)
}

View File

@ -45,6 +45,7 @@ kotlin {
implementation(libs.voyager.navigator)
implementation(libs.voyager.tab)
implementation(libs.compose.imageloader)
implementation(projects.resources)
implementation(projects.coreAppearance)

View File

@ -11,6 +11,7 @@ actual val homeTabModule = module {
HomeScreenModel(
mvi = DefaultMviModel(HomeScreenMviModel.UiState()),
postsRepository = get(),
communityRepository = get(),
apiConfigRepository = get(),
)
}

View File

@ -6,6 +6,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.ApiConfigurationRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.CommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.PostsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
@ -14,6 +15,7 @@ import kotlinx.coroutines.launch
class HomeScreenModel(
private val mvi: DefaultMviModel<HomeScreenMviModel.Intent, HomeScreenMviModel.UiState, HomeScreenMviModel.Effect>,
private val postsRepository: PostsRepository,
private val communityRepository: CommunityRepository,
private val apiConfigRepository: ApiConfigurationRepository,
) : ScreenModel,
MviModel<HomeScreenMviModel.Intent, HomeScreenMviModel.UiState, HomeScreenMviModel.Effect> by mvi {
@ -56,12 +58,30 @@ class HomeScreenModel(
page = currentPage,
type = type,
sort = sort,
)
).map {
val community = it.community
if (community?.id != null) {
val remoteCommunity = communityRepository.getCommunity(community.id)
it.copy(
community = it.community?.copy(
name = remoteCommunity?.name.orEmpty(),
icon = remoteCommunity?.icon,
)
)
} else {
it
}
}
currentPage++
val canFetchMore = postList.size >= PostsRepository.DEFAULT_PAGE_SIZE
mvi.updateState {
val newPosts = if (refreshing) {
postList
} else {
it.posts + postList
}
it.copy(
posts = if (refreshing) postList else it.posts + postList,
posts = newPosts,
loading = false,
canFetchMore = canFetchMore,
refreshing = false,

View File

@ -1,49 +1,36 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_home
import androidx.compose.foundation.Image
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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SpaceDashboard
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.racconforlemmy.core_utils.onClick
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.theme.CornerSize
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core_md.compose.Markdown
import com.github.diegoberaldin.raccoonforlemmy.feature_home.modals.ListingTypeBottomSheet
import com.github.diegoberaldin.raccoonforlemmy.feature_home.modals.SortBottomSheet
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
@ -81,7 +68,27 @@ object HomeTab : Tab {
Scaffold(
modifier = Modifier.padding(Spacing.xxs),
topBar = {
PostsTopBar(model, uiState)
PostsTopBar(
currentInstance = uiState.instance,
listingType = uiState.listingType,
sortType = uiState.sortType,
onSelectListingType = {
bottomSheetChannel.trySend @Composable {
ListingTypeBottomSheet { type ->
model.reduce(HomeScreenMviModel.Intent.ChangeListing(type))
bottomSheetChannel.trySend(null)
}
}
},
onSelectSortType = {
bottomSheetChannel.trySend @Composable {
SortBottomSheet { type ->
model.reduce(HomeScreenMviModel.Intent.ChangeSort(type))
bottomSheetChannel.trySend(null)
}
}
},
)
}
) {
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
@ -95,25 +102,7 @@ object HomeTab : Tab {
verticalArrangement = Arrangement.spacedBy(Spacing.xs)
) {
items(uiState.posts) { post ->
Card(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(CornerSize.m)
).padding(Spacing.s)
) {
Column {
Text(
text = post.title,
style = MaterialTheme.typography.titleMedium,
)
val body = post.text
if (body.isNotEmpty()) {
Markdown(content = body)
}
}
}
PostCard(post)
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
@ -143,65 +132,4 @@ object HomeTab : Tab {
}
}
}
@Composable
private fun PostsTopBar(
model: HomeScreenModel,
uiState: HomeScreenMviModel.UiState,
) {
Row(
modifier = Modifier.height(64.dp).padding(Spacing.s),
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier.onClick {
bottomSheetChannel.trySend @Composable {
ListingTypeBottomSheet { type ->
model.reduce(HomeScreenMviModel.Intent.ChangeListing(type))
bottomSheetChannel.trySend(null)
}
}
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Spacing.m),
) {
Image(
imageVector = uiState.listingType.toIcon(),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
)
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.xxxs)
) {
Text(
text = uiState.listingType.toReadableName(),
style = MaterialTheme.typography.titleMedium
)
Text(
text = stringResource(
MR.strings.home_instance_via,
uiState.instance
),
style = MaterialTheme.typography.titleSmall
)
}
}
Spacer(modifier = Modifier.weight(1f))
Image(
modifier = Modifier.onClick {
bottomSheetChannel.trySend @Composable {
SortBottomSheet { type ->
model.reduce(HomeScreenMviModel.Intent.ChangeSort(type))
bottomSheetChannel.trySend(null)
}
}
},
imageVector = uiState.sortType.toIcon(),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
)
}
}
}

View File

@ -0,0 +1,128 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_home
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.ArrowDropUp
import androidx.compose.material.icons.filled.Chat
import androidx.compose.material3.Card
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
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.theme.CornerSize
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core_md.compose.Markdown
import com.github.diegoberaldin.raccoonforlemmy.data.PostModel
import com.seiko.imageloader.rememberImagePainter
@Composable
fun PostCard(
post: PostModel,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(CornerSize.m)
).padding(
vertical = Spacing.lHalf,
horizontal = Spacing.s,
)
) {
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.s)
) {
Text(
text = post.title,
style = MaterialTheme.typography.titleMedium,
)
val communityName = post.community?.name.orEmpty()
val communityIcon = post.community?.icon.orEmpty()
val iconSize = 21.dp
if (communityName.isNotEmpty()) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Spacing.s),
) {
if (communityIcon.isNotEmpty()) {
val painter = rememberImagePainter(communityIcon)
Image(
modifier = Modifier.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2)),
painter = painter,
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
}
Text(
text = communityName,
style = MaterialTheme.typography.titleSmall,
)
}
}
val imageUrl = post.thumbnailUrl
if (!imageUrl.isNullOrEmpty()) {
val painter = rememberImagePainter(imageUrl)
Image(
modifier = Modifier.fillMaxWidth(),
painter = painter,
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
}
val body = post.text
if (body.isNotEmpty()) {
Markdown(content = body)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Spacing.xxs),
) {
val buttonModifier = Modifier.size(42.dp)
Image(
modifier = buttonModifier,
imageVector = Icons.Default.ArrowDropUp,
contentDescription = null,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface)
)
Text(
text = "${post.score}"
)
Image(
modifier = buttonModifier,
imageVector = Icons.Default.ArrowDropDown,
contentDescription = null,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface)
)
Spacer(modifier = Modifier.weight(1f))
Image(
modifier = buttonModifier.padding(10.dp),
imageVector = Icons.Default.Chat,
contentDescription = null,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface)
)
Text(
text = "${post.comments}"
)
}
}
}
}

View File

@ -0,0 +1,76 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_home
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.racconforlemmy.core_utils.onClick
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
@Composable
internal fun PostsTopBar(
currentInstance: String,
listingType: ListingType,
sortType: SortType,
onSelectListingType: () -> Unit,
onSelectSortType: () -> Unit,
) {
Row(
modifier = Modifier.height(64.dp).padding(Spacing.s),
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier.onClick {
onSelectListingType()
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Spacing.m),
) {
Image(
imageVector = listingType.toIcon(),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
)
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.xxxs)
) {
Text(
text = listingType.toReadableName(),
style = MaterialTheme.typography.titleMedium
)
Text(
text = stringResource(
MR.strings.home_instance_via,
currentInstance,
),
style = MaterialTheme.typography.titleSmall
)
}
}
Spacer(modifier = Modifier.weight(1f))
Image(
modifier = Modifier.onClick {
onSelectSortType()
},
imageVector = sortType.toIcon(),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
)
}
}

View File

@ -38,6 +38,7 @@ kotlin {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.materialIconsExtended)
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources)

View File

@ -3,7 +3,7 @@ package com.github.diegoberaldin.raccoonforlemmy.feature_search
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Explore
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -23,7 +23,7 @@ object SearchTab : Tab {
@Composable
get() {
val title = stringResource(MR.strings.navigation_search)
val icon = rememberVectorPainter(Icons.Default.Search)
val icon = rememberVectorPainter(Icons.Default.Explore)
return remember {
TabOptions(