WIP perfil en compose
This commit is contained in:
parent
37837653cc
commit
b019c79bcb
@ -4,7 +4,6 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@ -34,13 +33,12 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.posting.posts.domain.entity.Post
|
||||
import com.isolaatti.utils.UrlGen.userProfileImage
|
||||
|
||||
@Composable
|
||||
fun Post(
|
||||
fun PostComponent(
|
||||
post: Post,
|
||||
onClick: () -> Unit = {},
|
||||
onLike: () -> Unit = {},
|
||||
@ -54,7 +52,7 @@ fun Post(
|
||||
onHashtagClick: (hashtag: String) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(modifier = modifier.padding(8.dp)) {
|
||||
Card(modifier = modifier.padding(8.dp).clickable { onClick() }) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
AsyncImage(
|
||||
model = userProfileImage(post.userId),
|
||||
@ -169,5 +167,5 @@ fun Post(
|
||||
@Composable
|
||||
@Preview(device = Devices.PIXEL_5)
|
||||
fun PostPreview() {
|
||||
Post(Post(id = 0L, textContent = "Test", userId = 1, privacy = 2, date = "Date", images = emptyList(), liked = false, userName = "Username", numberOfLikes = 1, numberOfComments = 2))
|
||||
PostComponent(Post(id = 0L, textContent = "Test", userId = 1, privacy = 2, date = "Date", images = emptyList(), liked = false, userName = "Username", numberOfLikes = 1, numberOfComments = 2))
|
||||
}
|
||||
@ -33,7 +33,6 @@ abstract class PostListingViewModelBase : ViewModel() {
|
||||
|
||||
val posts: MutableStateFlow<List<Post>> = MutableStateFlow(emptyList())
|
||||
|
||||
|
||||
val loadingPosts = MutableLiveData(false)
|
||||
|
||||
val noMoreContent = MutableLiveData(false)
|
||||
@ -114,6 +113,6 @@ abstract class PostListingViewModelBase : ViewModel() {
|
||||
}
|
||||
|
||||
fun onPostAddedAtTheBeginning(post: Post) {
|
||||
|
||||
posts.value = listOf(post) + posts.value
|
||||
}
|
||||
}
|
||||
@ -40,6 +40,7 @@ import com.isolaatti.hashtags.ui.HashtagsPostsActivity
|
||||
import com.isolaatti.home.ui.FeedFragment
|
||||
import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity
|
||||
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
|
||||
import com.isolaatti.posting.posts.components.PostComponent
|
||||
import com.isolaatti.posting.posts.domain.entity.Post
|
||||
import com.isolaatti.posting.posts.presentation.EditPostContract
|
||||
import com.isolaatti.posting.posts.presentation.PostListingViewModel
|
||||
@ -85,7 +86,7 @@ class PostListingFragment : Fragment() {
|
||||
LazyColumn(flingBehavior = ScrollableDefaults.flingBehavior()) {
|
||||
items(count = posts.size, key = {posts[it].id}) {
|
||||
val post = posts[it]
|
||||
com.isolaatti.posting.posts.components.Post(
|
||||
PostComponent(
|
||||
modifier = Modifier.animateItem(),
|
||||
post = post,
|
||||
onClick = {
|
||||
|
||||
@ -2,6 +2,7 @@ package com.isolaatti.profile.domain.entity
|
||||
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.common.Ownable
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.profile.data.remote.UserProfileDto
|
||||
import com.isolaatti.utils.UrlGen
|
||||
import java.io.Serializable
|
||||
@ -26,6 +27,8 @@ data class UserProfile(
|
||||
|
||||
val profileAvatarPictureUrl: String get() = UrlGen.userProfileImage(userId)
|
||||
val profilePictureUrl: String get() = UrlGen.userProfileImageFullQuality(userId)
|
||||
|
||||
val profileImage: RemoteImage? get() = profileImageId?.let { imageId -> RemoteImage(imageId) }
|
||||
companion object {
|
||||
fun fromDto(userProfileDto: UserProfileDto): UserProfile {
|
||||
return UserProfile(
|
||||
|
||||
@ -16,6 +16,7 @@ import com.isolaatti.profile.domain.use_case.SetProfileImage
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@ -32,6 +33,7 @@ class ProfileViewModel @Inject constructor(
|
||||
) : PostListingViewModelBase() {
|
||||
private val _profile = MutableLiveData<UserProfile>()
|
||||
val profile: LiveData<UserProfile> get() = _profile
|
||||
val loadingProfile: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||
|
||||
var profileId: Int = 0
|
||||
|
||||
@ -41,6 +43,8 @@ class ProfileViewModel @Inject constructor(
|
||||
|
||||
val followingLoading: MutableLiveData<Boolean> = MutableLiveData()
|
||||
|
||||
val isRefreshing: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
|
||||
|
||||
// runs the lists of "Runnable" one by one and clears list. After this is executed,
|
||||
// caller should report as handled
|
||||
@ -57,13 +61,17 @@ class ProfileViewModel @Inject constructor(
|
||||
getProfileUseCase(profileId).onEach {
|
||||
when(it) {
|
||||
is Resource.Error -> {
|
||||
loadingProfile.value = false
|
||||
errorLoading.postValue(it.errorType)
|
||||
toRetry.add {
|
||||
getProfile()
|
||||
}
|
||||
}
|
||||
is Resource.Loading -> {}
|
||||
is Resource.Loading -> {
|
||||
loadingProfile.value = true
|
||||
}
|
||||
is Resource.Success -> {
|
||||
loadingProfile.value = false
|
||||
_profile.postValue(it.data!!)
|
||||
followingState.postValue(
|
||||
it.data.let { user-> getFollowingState(user.followingThisUser, user.thisUserIsFollowingMe) }
|
||||
@ -118,20 +126,34 @@ class ProfileViewModel @Inject constructor(
|
||||
if(refresh) {
|
||||
posts.value = emptyList()
|
||||
}
|
||||
Log.d(TAG, "getFeed")
|
||||
getProfilePostsUseCase(profileId, getLastId(), false, null).onEach { feedDtoResource ->
|
||||
when (feedDtoResource) {
|
||||
is Resource.Success -> {
|
||||
loadingPosts.postValue(false)
|
||||
if(refresh) {
|
||||
isRefreshing.value = false
|
||||
} else {
|
||||
loadingPosts.postValue(false)
|
||||
}
|
||||
|
||||
posts.value += feedDtoResource.data ?: emptyList()
|
||||
noMoreContent.postValue(feedDtoResource.data?.size == 0)
|
||||
}
|
||||
|
||||
is Resource.Loading -> {
|
||||
loadingPosts.postValue(true)
|
||||
if(refresh) {
|
||||
isRefreshing.value = true
|
||||
} else {
|
||||
loadingPosts.postValue(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
is Resource.Error -> {
|
||||
if(refresh) {
|
||||
isRefreshing.value = true
|
||||
} else {
|
||||
loadingPosts.postValue(true)
|
||||
}
|
||||
errorLoading.postValue(feedDtoResource.errorType)
|
||||
toRetry.add {
|
||||
getFeed(refresh, hashtag)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.isolaatti.profile.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
@ -8,72 +8,89 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import coil3.load
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import androidx.navigation.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.isolaatti.BuildConfig
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.common.CoilImageLoader.imageLoader
|
||||
import com.isolaatti.audio.player.MediaService
|
||||
import com.isolaatti.common.Dialogs
|
||||
import com.isolaatti.common.ErrorMessageViewModel
|
||||
import com.isolaatti.common.Ownable
|
||||
import com.isolaatti.common.options_bottom_sheet.domain.OptionClicked
|
||||
import com.isolaatti.common.IsolaattiTheme
|
||||
import com.isolaatti.common.options_bottom_sheet.domain.Options
|
||||
import com.isolaatti.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
|
||||
import com.isolaatti.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
|
||||
import com.isolaatti.databinding.FragmentDiscussionsBinding
|
||||
import com.isolaatti.followers.domain.FollowingState
|
||||
import com.isolaatti.hashtags.ui.HashtagsPostsActivity
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity
|
||||
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
|
||||
import com.isolaatti.posting.posts.components.PostComponent
|
||||
import com.isolaatti.posting.posts.domain.entity.Post
|
||||
import com.isolaatti.posting.posts.presentation.CreatePostContract
|
||||
import com.isolaatti.posting.posts.presentation.EditPostContract
|
||||
import com.isolaatti.posting.posts.presentation.PostListingRecyclerViewAdapterWiring
|
||||
import com.isolaatti.posting.posts.ui.PostInfoActivity
|
||||
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
|
||||
import com.isolaatti.profile.domain.entity.UserProfile
|
||||
import com.isolaatti.profile.presentation.EditProfileContract
|
||||
import com.isolaatti.profile.presentation.ProfileViewModel
|
||||
import com.isolaatti.profile.ui.components.ProfileHeader
|
||||
import com.isolaatti.reports.data.ContentType
|
||||
import com.isolaatti.reports.ui.NewReportBottomSheetDialogFragment
|
||||
import com.isolaatti.utils.UrlGen
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.noties.markwon.AbstractMarkwonPlugin
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.MarkwonConfiguration
|
||||
import io.noties.markwon.image.coil.CoilImagesPlugin
|
||||
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
|
||||
import io.noties.markwon.linkify.LinkifyPlugin
|
||||
import kotlinx.coroutines.guava.await
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ProfileMainFragment : Fragment() {
|
||||
lateinit var viewBinding: FragmentDiscussionsBinding
|
||||
private val viewModel: ProfileViewModel by viewModels()
|
||||
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
|
||||
val errorViewModel: ErrorMessageViewModel by activityViewModels()
|
||||
private var userId: Int? = null
|
||||
|
||||
|
||||
private var audioDescriptionAudio: Audio? = null
|
||||
|
||||
|
||||
// collapsing bar
|
||||
private var title = ""
|
||||
private var scrollRange = -1
|
||||
private var isShow = false
|
||||
|
||||
private val createDiscussion = registerForActivityResult(CreatePostContract()) {
|
||||
if(it == null)
|
||||
return@registerForActivityResult
|
||||
@ -83,13 +100,6 @@ class ProfileMainFragment : Fragment() {
|
||||
viewModel.onPostAddedAtTheBeginning(it)
|
||||
}
|
||||
|
||||
|
||||
private val editDiscussion = registerForActivityResult(EditPostContract()) {
|
||||
if(it != null) {
|
||||
viewModel.onPostUpdate(it)
|
||||
}
|
||||
}
|
||||
|
||||
private val editProfile = registerForActivityResult(EditProfileContract()) { updatedProfile ->
|
||||
if(updatedProfile != null) {
|
||||
viewModel.setProfile(updatedProfile)
|
||||
@ -97,121 +107,324 @@ class ProfileMainFragment : Fragment() {
|
||||
}
|
||||
|
||||
|
||||
private val profileObserver = Observer<UserProfile> { profile ->
|
||||
viewBinding.profileImageView.load(UrlGen.userProfileImage(profile.userId, invalidateCache = true), imageLoader)
|
||||
|
||||
title = profile.name
|
||||
viewBinding.textViewUsername.text = profile.name
|
||||
viewBinding.textViewDescription.text = profile.descriptionText
|
||||
if(profile.descriptionText.isNullOrBlank()) {
|
||||
viewBinding.descriptionCard.visibility = View.GONE
|
||||
}
|
||||
private fun getData() {
|
||||
|
||||
viewBinding.goToFollowersBtn.text = getString(
|
||||
R.string.go_to_followers_btn_text,
|
||||
profile.numberOfFollowers.toString(),
|
||||
profile.numberOfFollowing.toString()
|
||||
)
|
||||
|
||||
|
||||
viewBinding.profileImageView.setOnClickListener {
|
||||
optionsViewModel.setOptions(Options.PROFILE_PHOTO_OPTIONS, CALLER_ID, profile)
|
||||
val fragment = BottomSheetPostOptionsFragment()
|
||||
fragment.show(parentFragmentManager, BottomSheetPostOptionsFragment.TAG)
|
||||
}
|
||||
|
||||
audioDescriptionAudio = profile.descriptionAudio
|
||||
viewBinding.playButtonContainer.visibility = if(profile.descriptionAudio != null) View.VISIBLE else View.GONE
|
||||
|
||||
setupUiForUserType(profile.isUserItself)
|
||||
}
|
||||
|
||||
// private val postsObserver: Observer<Pair<List<Post>?, UpdateEvent>?> = Observer {
|
||||
// if(it?.first != null) {
|
||||
// postsAdapter.updateList(it.first!!, it.second)
|
||||
// postsAdapter.newContentRequestFinished()
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
private val followingStateObserver: Observer<FollowingState> = Observer {
|
||||
when(it) {
|
||||
FollowingState.FollowingThisUser -> {
|
||||
viewBinding.textViewFollowingState.setText(R.string.following_user)
|
||||
viewBinding.followButton.setText(R.string.unfollow)
|
||||
viewBinding.followButton.isChecked = true
|
||||
}
|
||||
FollowingState.MutuallyFollowing -> {
|
||||
viewBinding.textViewFollowingState.setText(R.string.mutually_following)
|
||||
viewBinding.followButton.setText(R.string.unfollow)
|
||||
viewBinding.followButton.isChecked = true
|
||||
}
|
||||
FollowingState.ThisUserIsFollowingMe -> {
|
||||
viewBinding.textViewFollowingState.setText(R.string.following_you)
|
||||
viewBinding.followButton.setText(R.string.follow)
|
||||
viewBinding.followButton.isChecked = false
|
||||
}
|
||||
FollowingState.NotMutuallyFollowing -> {
|
||||
viewBinding.textViewFollowingState.text = ""
|
||||
viewBinding.followButton.setText(R.string.follow)
|
||||
viewBinding.followButton.isChecked = false
|
||||
}
|
||||
userId?.let { profileId ->
|
||||
viewModel.profileId = profileId
|
||||
viewModel.getProfile()
|
||||
viewModel.getFeed(true, null)
|
||||
}
|
||||
}
|
||||
|
||||
private val optionsObserver: Observer<OptionClicked?> = Observer { optionClicked ->
|
||||
if(optionClicked?.callerId == CALLER_ID) {
|
||||
Log.d("ProfileMainFragment", optionClicked.toString())
|
||||
private lateinit var mediaControllerFuture: ListenableFuture<MediaController>
|
||||
private lateinit var mediaController: MediaController
|
||||
|
||||
when(optionClicked.optionsId) {
|
||||
Options.PROFILE_PHOTO_OPTIONS -> {
|
||||
val profile = optionClicked.payload as? UserProfile
|
||||
when(optionClicked.optionId) {
|
||||
Options.Option.OPTION_PROFILE_PHOTO_CHANGE_PHOTO -> {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), MediaService::class.java))
|
||||
mediaControllerFuture = MediaController.Builder(requireContext(), sessionToken).buildAsync()
|
||||
|
||||
userId = (requireActivity()).intent.extras?.getInt(ProfileActivity.EXTRA_USER_ID)
|
||||
getData()
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
|
||||
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
val isRefreshing by viewModel.isRefreshing.collectAsState()
|
||||
val lazyColumState = rememberLazyListState()
|
||||
val profile by viewModel.profile.observeAsState()
|
||||
val followingState by viewModel.followingState.observeAsState()
|
||||
val isAtTop by remember {
|
||||
derivedStateOf {
|
||||
lazyColumState.firstVisibleItemIndex == 0 && lazyColumState.firstVisibleItemScrollOffset == 0
|
||||
}
|
||||
}
|
||||
var showAddPostButton by remember { mutableStateOf(false) }
|
||||
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
// Hide FAB
|
||||
if (available.y < -1) {
|
||||
showAddPostButton = false
|
||||
}
|
||||
|
||||
// Show FAB
|
||||
if (available.y > 1) {
|
||||
showAddPostButton = true
|
||||
}
|
||||
|
||||
return Offset.Zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(profile) {
|
||||
showAddPostButton = profile?.isUserItself == true
|
||||
}
|
||||
|
||||
val followingText = when(followingState) {
|
||||
FollowingState.FollowingThisUser -> {
|
||||
stringResource(R.string.following_user)
|
||||
}
|
||||
FollowingState.MutuallyFollowing -> {
|
||||
stringResource(R.string.mutually_following)
|
||||
}
|
||||
FollowingState.ThisUserIsFollowingMe -> {
|
||||
stringResource(R.string.following_you)
|
||||
}
|
||||
FollowingState.NotMutuallyFollowing -> {
|
||||
""
|
||||
}
|
||||
|
||||
null -> ""
|
||||
}
|
||||
|
||||
|
||||
val posts by viewModel.posts.collectAsState()
|
||||
val loadingProfile by viewModel.loadingProfile.collectAsState()
|
||||
|
||||
val playingAudio by remember { mutableStateOf(true) }
|
||||
IsolaattiTheme {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
activity?.finish()
|
||||
}
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.back_button))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(if(isAtTop) stringResource(R.string.profile) else profile?.name ?: "")
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
profile?.let { findNavController().navigate(ProfileMainFragmentDirections.actionMainFragmentToQrCodeFragment(it)) }
|
||||
}
|
||||
) {
|
||||
Icon(painterResource(R.drawable.baseline_qr_code_24), stringResource(R.string.qr_code))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = showAddPostButton,
|
||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||
exit = slideOutVertically(targetOffsetY = { it * 2 })
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
createDiscussion.launch(Unit)
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.Add, stringResource(R.string.add_post_button_desc))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Options.Option.OPTION_PROFILE_PHOTO_REMOVE_PHOTO -> {
|
||||
showRemoveProfileImageDialog()
|
||||
}
|
||||
Options.Option.OPTION_PROFILE_PHOTO_VIEW_PHOTO -> {
|
||||
val profilePictureUrl = profile?.profilePictureUrl
|
||||
if(profilePictureUrl != null) {
|
||||
PictureViewerActivity.startActivityWithImages(requireContext(), arrayOf(
|
||||
RemoteImage(profile.profileImageId ?: "")
|
||||
))
|
||||
) { insets ->
|
||||
|
||||
PullToRefreshBox(
|
||||
isRefreshing,
|
||||
onRefresh = {
|
||||
viewModel.getProfile()
|
||||
viewModel.getFeed(refresh = true, hashtag = null)
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(insets)
|
||||
.consumeWindowInsets(insets)
|
||||
) {
|
||||
LazyColumn(
|
||||
state = lazyColumState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if(profile?.isUserItself == true) { Modifier.nestedScroll(nestedScrollConnection) } else Modifier)
|
||||
) {
|
||||
item {
|
||||
profile?.let {
|
||||
ProfileHeader(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
name = it.name,
|
||||
description = it.descriptionText,
|
||||
image = it.profileImage,
|
||||
audio = it.descriptionAudio,
|
||||
followingCount = it.numberOfFollowing,
|
||||
followerCount = it.numberOfFollowers,
|
||||
loading = loadingProfile,
|
||||
onImageClick = {
|
||||
optionsViewModel.setOptions(Options.PROFILE_PHOTO_OPTIONS, CALLER_ID, it)
|
||||
val fragment = BottomSheetPostOptionsFragment()
|
||||
fragment.show(parentFragmentManager, BottomSheetPostOptionsFragment.TAG)
|
||||
},
|
||||
onFollowersClick = {
|
||||
findNavController().navigate(ProfileMainFragmentDirections.actionDiscussionsFragmentToMainFollowersFragment(it.userId))
|
||||
},
|
||||
followingText = followingText,
|
||||
followingThisUser = it.followingThisUser,
|
||||
isOwnUser = it.isUserItself,
|
||||
onFollowChange = { follow ->
|
||||
viewModel.followUser()
|
||||
},
|
||||
profileAudioProgress = 0.4f,
|
||||
showProfileAudioProgress = true,
|
||||
onEditProfileClick = {
|
||||
editProfile.launch(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if(posts.isEmpty()) {
|
||||
item {
|
||||
Text(getString(R.string.you_will_see_what_user_posts_here, profile?.name ?: ""), modifier = Modifier.padding(24.dp))
|
||||
}
|
||||
}
|
||||
|
||||
items(count = posts.size, key = {posts[it].id}) {
|
||||
val post = posts[it]
|
||||
PostComponent(
|
||||
modifier = Modifier.animateItem(),
|
||||
post = post,
|
||||
onClick = {
|
||||
PostViewerActivity.startActivity(requireContext(), post.id)
|
||||
},
|
||||
onComments = {
|
||||
val modalBottomSheet = BottomSheetPostComments.getInstance(post.id)
|
||||
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostComments.TAG)
|
||||
},
|
||||
onOptions = {
|
||||
optionsViewModel.setOptions(Options.POST_OPTIONS, CALLER_ID, post)
|
||||
val modalBottomSheet = BottomSheetPostOptionsFragment()
|
||||
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG)
|
||||
},
|
||||
onInfo = {
|
||||
PostInfoActivity.startActivity(requireContext(), post.id)
|
||||
},
|
||||
onLike = {
|
||||
viewModel.likePost(post.id)
|
||||
},
|
||||
onDislike = {
|
||||
viewModel.unLikePost(post.id)
|
||||
},
|
||||
onShare = {
|
||||
val intent = Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, "${BuildConfig.backend}/pub/${post.id}")
|
||||
type = "text/plain"
|
||||
}, getString(R.string.share_post))
|
||||
startActivity(intent)
|
||||
},
|
||||
onImageClick = { images, index ->
|
||||
PictureViewerActivity.startActivityWithImages(requireContext(), images.toTypedArray(), index)
|
||||
},
|
||||
onUsernameClick = {
|
||||
if(profile?.userId == post.userId) return@PostComponent
|
||||
ProfileActivity.startActivity(requireContext(), post.userId)
|
||||
},
|
||||
onHashtagClick = { hashtag ->
|
||||
HashtagsPostsActivity.startActivity(requireContext(), hashtag)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
optionsViewModel.handle()
|
||||
}
|
||||
Options.POST_OPTIONS -> {
|
||||
// post id should come as payload
|
||||
val post = optionClicked.payload as? Post ?: return@Observer
|
||||
when(optionClicked.optionId) {
|
||||
Options.Option.OPTION_DELETE -> {
|
||||
Dialogs.buildDeletePostDialog(requireContext()) { delete ->
|
||||
optionsViewModel.handle()
|
||||
if(delete) {
|
||||
viewModel.deletePost(post.id)
|
||||
}
|
||||
}.show()
|
||||
|
||||
}
|
||||
Options.Option.OPTION_EDIT -> {
|
||||
optionsViewModel.handle()
|
||||
editDiscussion.launch(post.id)
|
||||
}
|
||||
Options.Option.OPTION_REPORT -> {
|
||||
optionsViewModel.handle()
|
||||
NewReportBottomSheetDialogFragment.newInstance(ContentType.Post, post.id.toString()).show(childFragmentManager, NewReportBottomSheetDialogFragment.LOG_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
Options.PROFILE_DESCRIPTION_OPTIONS -> {
|
||||
optionsViewModel.handle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
mediaController = mediaControllerFuture.await()
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
errorViewModel.retry.collect {
|
||||
viewModel.retry()
|
||||
errorViewModel.handleRetry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
optionsViewModel.optionClicked.observe(viewLifecycleOwner) { optionClicked ->
|
||||
if(optionClicked?.callerId == CALLER_ID) {
|
||||
Log.d("ProfileMainFragment", optionClicked.toString())
|
||||
|
||||
when(optionClicked.optionsId) {
|
||||
Options.PROFILE_PHOTO_OPTIONS -> {
|
||||
val profile = optionClicked.payload as? UserProfile
|
||||
when(optionClicked.optionId) {
|
||||
Options.Option.OPTION_PROFILE_PHOTO_CHANGE_PHOTO -> {
|
||||
|
||||
}
|
||||
Options.Option.OPTION_PROFILE_PHOTO_REMOVE_PHOTO -> {
|
||||
showRemoveProfileImageDialog()
|
||||
}
|
||||
Options.Option.OPTION_PROFILE_PHOTO_VIEW_PHOTO -> {
|
||||
profile?.profileImage?.let {
|
||||
PictureViewerActivity.startActivityWithImages(requireContext(), arrayOf(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
optionsViewModel.handle()
|
||||
}
|
||||
Options.POST_OPTIONS -> {
|
||||
// post id should come as payload
|
||||
val post = optionClicked.payload as? Post ?: return@observe
|
||||
when(optionClicked.optionId) {
|
||||
Options.Option.OPTION_DELETE -> {
|
||||
Dialogs.buildDeletePostDialog(requireContext()) { delete ->
|
||||
optionsViewModel.handle()
|
||||
if(delete) {
|
||||
viewModel.deletePost(post.id)
|
||||
}
|
||||
}.show()
|
||||
|
||||
}
|
||||
Options.Option.OPTION_EDIT -> {
|
||||
optionsViewModel.handle()
|
||||
//editDiscussion.launch(post.id)
|
||||
}
|
||||
Options.Option.OPTION_REPORT -> {
|
||||
NewReportBottomSheetDialogFragment
|
||||
.newInstance(ContentType.Post, post.id.toString())
|
||||
.show(childFragmentManager, NewReportBottomSheetDialogFragment.LOG_TAG)
|
||||
optionsViewModel.handle()
|
||||
}
|
||||
}
|
||||
}
|
||||
Options.PROFILE_DESCRIPTION_OPTIONS -> {
|
||||
optionsViewModel.handle()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -226,254 +439,6 @@ class ProfileMainFragment : Fragment() {
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private lateinit var postListingRecyclerViewAdapterWiring: PostListingRecyclerViewAdapterWiring
|
||||
|
||||
// counter variable to prevent calling appBarLayout.totalScrollRange many times
|
||||
private var i = 0
|
||||
private fun setupCollapsingBar() {
|
||||
viewBinding.topAppBarLayout.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
|
||||
// prevent calling appBarLayout.totalScrollRange many times
|
||||
if (i == 30) {
|
||||
scrollRange = appBarLayout.totalScrollRange
|
||||
i = 0
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
|
||||
if (scrollRange + verticalOffset == 0) {
|
||||
viewBinding.collapsingToolbarLayout.title = viewModel.profile.value?.name
|
||||
isShow = true
|
||||
} else if (isShow) {
|
||||
viewBinding.collapsingToolbarLayout.title = " "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBlockUserDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.block_profile)
|
||||
.setMessage(getString(R.string.block_profile_dialog_message, viewModel.profile.value?.name ?: ""))
|
||||
.setPositiveButton(R.string.yes_continue) {_, _ ->
|
||||
// TODO implement
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
|
||||
}
|
||||
|
||||
private fun bind() {
|
||||
|
||||
if(userId == null) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
viewBinding.topAppBar.setNavigationOnClickListener {
|
||||
requireActivity().finish()
|
||||
}
|
||||
|
||||
viewBinding.topAppBar.setOnMenuItemClickListener {
|
||||
when(it.itemId) {
|
||||
R.id.edit_profile -> {
|
||||
viewModel.profile.value?.let { profile -> editProfile.launch(profile) }
|
||||
|
||||
true
|
||||
}
|
||||
R.id.user_link_menu_item -> {
|
||||
viewModel.profile.value?.let { profile ->
|
||||
findNavController().navigate(ProfileMainFragmentDirections.actionMainFragmentToQrCodeFragment(profile))
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
R.id.report_profile_menu_item -> {
|
||||
NewReportBottomSheetDialogFragment.newInstance(ContentType.Profile, viewModel.profileId.toString()).show(childFragmentManager, NewReportBottomSheetDialogFragment.LOG_TAG)
|
||||
true
|
||||
}
|
||||
R.id.block_profile_menu_item -> {
|
||||
showBlockUserDialog()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
viewBinding.createPostButton.setOnClickListener {
|
||||
createDiscussion.launch(Unit)
|
||||
}
|
||||
|
||||
viewBinding.goToFollowersBtn.setOnClickListener {
|
||||
findNavController().navigate(ProfileMainFragmentDirections.actionDiscussionsFragmentToMainFollowersFragment(userId!!))
|
||||
}
|
||||
|
||||
// viewBinding.feedRecyclerView.adapter = postsAdapter
|
||||
// viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
|
||||
|
||||
viewBinding.swipeToRefresh.setOnRefreshListener {
|
||||
viewModel.getFeed(true, null)
|
||||
}
|
||||
viewBinding.playButton.setOnClickListener {
|
||||
audioDescriptionAudio?.let { audio ->
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
viewBinding.followButton.setOnClickListener {
|
||||
it.isEnabled = false
|
||||
viewModel.followUser()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setObservers() {
|
||||
viewModel.profile.observe(viewLifecycleOwner, profileObserver)
|
||||
//viewModel.posts.observe(viewLifecycleOwner, postsObserver)
|
||||
viewModel.followingState.observe(viewLifecycleOwner, followingStateObserver)
|
||||
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver)
|
||||
viewModel.loadingPosts.observe(viewLifecycleOwner) {
|
||||
viewBinding.loadingIndicator.visibility = if(it) View.VISIBLE else View.GONE
|
||||
if(!it) {
|
||||
viewBinding.swipeToRefresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
viewModel.errorLoading.observe(viewLifecycleOwner) {
|
||||
errorViewModel.error.postValue(it)
|
||||
}
|
||||
viewModel.followingLoading.observe(viewLifecycleOwner) {
|
||||
viewBinding.followButton.isEnabled = !it
|
||||
}
|
||||
}
|
||||
|
||||
private fun getData() {
|
||||
|
||||
userId?.let { profileId ->
|
||||
viewModel.profileId = profileId
|
||||
viewModel.getProfile()
|
||||
viewModel.getFeed(true, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupPostsAdapter() {
|
||||
val markwon = Markwon.builder(requireContext())
|
||||
.usePlugin(object: AbstractMarkwonPlugin() {
|
||||
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
||||
builder
|
||||
.imageDestinationProcessor(
|
||||
ImageDestinationProcessorRelativeToAbsolute
|
||||
.create(BuildConfig.backend))
|
||||
}
|
||||
})
|
||||
.usePlugin(LinkifyPlugin.create())
|
||||
.build()
|
||||
|
||||
//postsAdapter = PostsRecyclerViewAdapter(postListingRecyclerViewAdapterWiring )
|
||||
|
||||
}
|
||||
|
||||
private fun setupUiForUserType(isOwnProfile: Boolean) {
|
||||
if(isOwnProfile) {
|
||||
viewBinding.followButton.visibility = View.GONE
|
||||
viewBinding.topAppBar.menu?.run {
|
||||
removeItem(R.id.block_profile_menu_item)
|
||||
removeItem(R.id.report_profile_menu_item)
|
||||
}
|
||||
viewBinding.descriptionCard.setOnClickListener {
|
||||
optionsViewModel.setOptions(Options.PROFILE_DESCRIPTION_OPTIONS, CALLER_ID, null)
|
||||
val fragment = BottomSheetPostOptionsFragment()
|
||||
fragment.show(parentFragmentManager, BottomSheetPostOptionsFragment.TAG)
|
||||
}
|
||||
} else {
|
||||
viewBinding.createPostButton.visibility = View.GONE
|
||||
viewBinding.topAppBar.menu.removeItem(R.id.edit_profile)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
userId = (requireActivity()).intent.extras?.getInt(ProfileActivity.EXTRA_USER_ID)
|
||||
getData()
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
postListingRecyclerViewAdapterWiring = object: PostListingRecyclerViewAdapterWiring(viewModel) {
|
||||
override fun onComment(postId: Long) {
|
||||
val modalBottomSheet = BottomSheetPostComments.getInstance(postId)
|
||||
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostComments.TAG)
|
||||
}
|
||||
|
||||
override fun onOpenPost(postId: Long) {
|
||||
PostViewerActivity.startActivity(requireContext(), postId)
|
||||
}
|
||||
|
||||
override fun onOptions(post: Ownable) {
|
||||
optionsViewModel.setOptions(Options.POST_OPTIONS, CALLER_ID, post)
|
||||
val modalBottomSheet = BottomSheetPostOptionsFragment()
|
||||
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG)
|
||||
}
|
||||
|
||||
override fun onProfileClick(userId: Int) {
|
||||
//ProfileActivity.startActivity(requireContext(), userId)
|
||||
}
|
||||
|
||||
override fun onPlay(audio: Audio) {
|
||||
|
||||
}
|
||||
|
||||
override fun onLoadMore() {
|
||||
viewModel.getFeed(false, null)
|
||||
}
|
||||
|
||||
override fun onMoreInfo(postId: Long) {
|
||||
PostInfoActivity.startActivity(requireContext(), postId)
|
||||
}
|
||||
|
||||
override fun onShare(postId: Long) {
|
||||
val intent = Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, "${BuildConfig.backend}/pub/${postId}")
|
||||
type = "text/plain"
|
||||
}, getString(R.string.share_post))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun hashtagClicked(hashtag: String) {
|
||||
HashtagsPostsActivity.startActivity(requireContext(), hashtag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
viewBinding = FragmentDiscussionsBinding.inflate(inflater)
|
||||
|
||||
return viewBinding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupPostsAdapter()
|
||||
bind()
|
||||
setObservers()
|
||||
setupCollapsingBar()
|
||||
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
errorViewModel.retry.collect {
|
||||
viewModel.retry()
|
||||
errorViewModel.handleRetry()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CALLER_ID = 30
|
||||
const val LOG_TAG = "ProfileMainFragment"
|
||||
|
||||
@ -5,10 +5,15 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import androidmads.library.qrgenearator.BuildConfig
|
||||
import androidmads.library.qrgenearator.QRGContents
|
||||
import androidmads.library.qrgenearator.QRGEncoder
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
@ -49,6 +54,13 @@ class QrCodeFragment : Fragment() {
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
|
||||
val topPadding = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
binding.toolbar.updatePadding(top = topPadding.top)
|
||||
insets
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun generateUrl(): String {
|
||||
|
||||
@ -0,0 +1,173 @@
|
||||
package com.isolaatti.profile.ui.components
|
||||
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.VectorConverter
|
||||
import androidx.compose.animation.core.animateValue
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.Edit
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.images.common.domain.entity.RemoteImage
|
||||
|
||||
@Composable
|
||||
private fun ProfileHeaderLoading() {
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val tonalElevation by infiniteTransition.animateValue(
|
||||
initialValue = 6.dp,
|
||||
targetValue = 30.dp,
|
||||
typeConverter = Dp.VectorConverter,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(1000),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
), label = "tonal elevation for loading skeleton"
|
||||
)
|
||||
Surface(modifier = Modifier.fillMaxSize(), tonalElevation = tonalElevation) {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileHeader(
|
||||
name: String,
|
||||
description: String?,
|
||||
image: RemoteImage?,
|
||||
audio: Audio?,
|
||||
followingText: String?,
|
||||
followerCount: Int,
|
||||
followingCount: Int,
|
||||
onImageClick: () -> Unit,
|
||||
showProfileAudioProgress: Boolean,
|
||||
profileAudioProgress: Float,
|
||||
onFollowersClick: () -> Unit,
|
||||
onEditProfileClick: () -> Unit,
|
||||
onFollowChange: (Boolean) -> Unit,
|
||||
loading: Boolean = false,
|
||||
isOwnUser: Boolean,
|
||||
followingThisUser: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(modifier) {
|
||||
if(loading) {
|
||||
ProfileHeaderLoading()
|
||||
} else {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
|
||||
if(image != null) {
|
||||
AsyncImage(
|
||||
model = image.reducedImageUrl,
|
||||
contentDescription = stringResource(R.string.profile_photo),
|
||||
modifier = Modifier
|
||||
.size(140.dp)
|
||||
.padding(16.dp)
|
||||
.clip(RoundedCornerShape(50))
|
||||
.clickable {
|
||||
onImageClick()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painterResource(R.drawable.avatar),
|
||||
contentDescription = stringResource(R.string.profile_photo),
|
||||
modifier = Modifier
|
||||
.size(140.dp)
|
||||
.padding(16.dp)
|
||||
.clip(RoundedCornerShape(50))
|
||||
.clickable {
|
||||
onImageClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if(!isOwnUser) {
|
||||
followingText?.let {
|
||||
Text(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Text(name, fontSize = 20.sp, modifier = Modifier.padding(top = 16.dp))
|
||||
description?.let {
|
||||
Text(it, modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
|
||||
TextButton(onClick = onFollowersClick, modifier = Modifier.padding(top = 4.dp)) {
|
||||
Text(stringResource(R.string.go_to_followers_btn_text, followerCount, followingCount))
|
||||
}
|
||||
|
||||
when {
|
||||
isOwnUser -> {
|
||||
OutlinedButton(onClick = onEditProfileClick) {
|
||||
Icon(Icons.Default.Edit, null)
|
||||
Text(stringResource(R.string.edit_profile))
|
||||
}
|
||||
}
|
||||
|
||||
followingThisUser -> {
|
||||
OutlinedButton(onClick = { onFollowChange(false) }) {
|
||||
Text(stringResource(R.string.unfollow))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Button(onClick = { onFollowChange(true) }) {
|
||||
Text(stringResource(R.string.follow))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(device = Devices.PIXEL_3)
|
||||
fun ProfileHeaderPreview() {
|
||||
ProfileHeader(
|
||||
name = "Erik",
|
||||
description = "Hola",
|
||||
image = null,
|
||||
audio = null,
|
||||
followingText = "Mutually following",
|
||||
followerCount = 100,
|
||||
followingCount = 100,
|
||||
isOwnUser = true,
|
||||
followingThisUser = true,
|
||||
onImageClick = {},
|
||||
profileAudioProgress = 0.6f,
|
||||
showProfileAudioProgress = true,
|
||||
onFollowersClick = {},
|
||||
onFollowChange = {},
|
||||
onEditProfileClick = {}
|
||||
)
|
||||
}
|
||||
@ -58,62 +58,7 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/text_view_following_state"
|
||||
tools:text="Erik Cavazos" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/description_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
style="?attr/materialCardViewFilledStyle"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_view_username"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/play_button_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/audio_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/play_button"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
app:icon="@drawable/baseline_play_circle_24" />
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_view_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:maxLines="4"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/play_button_container"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Hi there, I am software developer!" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/go_to_followers_btn"
|
||||
@ -122,7 +67,7 @@
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
app:layout_constraintTop_toBottomOf="@id/description_card"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/text"/>
|
||||
|
||||
|
||||
|
||||
@ -175,17 +120,14 @@
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_to_refresh"
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/compose_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
android:clipToPadding="false">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/feed_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.compose.ui.platform.ComposeView>
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loading_indicator"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple">#4d3b68</color>
|
||||
<color name="purple">#A77EE4</color>
|
||||
<color name="purple_lighter">#3F0095</color>
|
||||
<color name="surface">#1D1725</color>
|
||||
<color name="on_surface">#FFFFFF</color>
|
||||
|
||||
@ -226,6 +226,10 @@
|
||||
<string name="pause_audio">Pause audio</string>
|
||||
<string name="play_audio">Play audio</string>
|
||||
<string name="just_recorded_audio_title">Just recorded audio</string>
|
||||
<string name="back_button">Back button</string>
|
||||
<string name="profile">Profile</string>
|
||||
<string name="add_post_button_desc">Add post button</string>
|
||||
<string name="you_will_see_what_user_posts_here">When %s posts something you will see it here</string>
|
||||
<string-array name="report_reasons">
|
||||
<item>Spam</item>
|
||||
<item>Explicit content</item>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user