WIP
This commit is contained in:
parent
46e12db1fd
commit
727007d94f
@ -0,0 +1,34 @@
|
|||||||
|
package com.isolaatti.audio.common.components
|
||||||
|
|
||||||
|
import android.media.MediaRecorder
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AudioRecorder(
|
||||||
|
onAudioRecorded: (Uri) -> Unit,
|
||||||
|
dismissible: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
var context = LocalContext.current
|
||||||
|
var mediaRecorder: MediaRecorder?
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
mediaRecorder = MediaRecorder()
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
mediaRecorder?.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AudioRecorderPreview() {
|
||||||
|
|
||||||
|
}
|
||||||
@ -21,7 +21,7 @@ interface ImagesApi {
|
|||||||
@POST("images/create")
|
@POST("images/create")
|
||||||
@Multipart
|
@Multipart
|
||||||
fun postImage(@Part file: MultipartBody.Part,
|
fun postImage(@Part file: MultipartBody.Part,
|
||||||
@Part name: MultipartBody.Part,
|
@Part postId: MultipartBody.Part,
|
||||||
@Part setAsProfile: MultipartBody.Part? = null,
|
@Part setAsProfile: MultipartBody.Part? = null,
|
||||||
@Part squadId: MultipartBody.Part? = null): Call<ImageDto>
|
@Part squadId: MultipartBody.Part? = null): Call<ImageDto>
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,7 @@ class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun uploadImage(name: String, imageUri: Uri, squadId: String?): Flow<Resource<Image>> = flow {
|
override fun uploadImage(imageUri: Uri, postId: Long, squadId: String?): Flow<Resource<Image>> = flow {
|
||||||
emit(Resource.Loading())
|
emit(Resource.Loading())
|
||||||
var imageInputStream: InputStream? = null
|
var imageInputStream: InputStream? = null
|
||||||
try {
|
try {
|
||||||
@ -84,8 +84,8 @@ class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi,
|
|||||||
|
|
||||||
|
|
||||||
val response = imagesApi.postImage(
|
val response = imagesApi.postImage(
|
||||||
MultipartBody.Part.createFormData("file", name,outputStream.toByteArray().toRequestBody()),
|
MultipartBody.Part.createFormData("file", "$postId-${System.currentTimeMillis()}",outputStream.toByteArray().toRequestBody()),
|
||||||
MultipartBody.Part.createFormData("name", name)
|
MultipartBody.Part.createFormData("postId", postId.toString())
|
||||||
).awaitResponse()
|
).awaitResponse()
|
||||||
if(response.isSuccessful) {
|
if(response.isSuccessful) {
|
||||||
val imageDto = response.body()
|
val imageDto = response.body()
|
||||||
@ -106,12 +106,4 @@ class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi,
|
|||||||
imageInputStream?.close()
|
imageInputStream?.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDetachedDraftImages(): Flow<Resource<List<ImageDraftEntity>>> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDraftImagesOfPost(postId: Long): Flow<Resource<List<ImageDraftEntity>>> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -9,7 +9,5 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
interface ImagesRepository {
|
interface ImagesRepository {
|
||||||
fun getImagesOfUser(userId: Int, lastId: String? = null): Flow<Resource<List<Image>>>
|
fun getImagesOfUser(userId: Int, lastId: String? = null): Flow<Resource<List<Image>>>
|
||||||
fun deleteImages(images: List<Image>): Flow<Resource<Boolean>>
|
fun deleteImages(images: List<Image>): Flow<Resource<Boolean>>
|
||||||
fun uploadImage(name: String, imageUri: Uri, squadId: String?): Flow<Resource<Image>>
|
fun uploadImage(imageUri: Uri, postId: Long, squadId: String?): Flow<Resource<Image>>
|
||||||
fun getDetachedDraftImages(): Flow<Resource<List<ImageDraftEntity>>>
|
|
||||||
fun getDraftImagesOfPost(postId: Long): Flow<Resource<List<ImageDraftEntity>>>
|
|
||||||
}
|
}
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package com.isolaatti.images.image_maker.presentation
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.isolaatti.images.common.domain.entity.Image
|
|
||||||
import com.isolaatti.images.common.domain.repository.ImagesRepository
|
|
||||||
import com.isolaatti.utils.Resource
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class ImageMakerViewModel @Inject constructor(private val imagesRepository: ImagesRepository) : ViewModel() {
|
|
||||||
var imageUri: Uri? = null
|
|
||||||
var name: String? = null
|
|
||||||
val image: MutableLiveData<Resource<Image>> = MutableLiveData()
|
|
||||||
fun uploadPicture() {
|
|
||||||
if(imageUri == null || name == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
|
||||||
imagesRepository.uploadImage(name!!.trim(), imageUri!!, null).onEach {
|
|
||||||
image.postValue(it)
|
|
||||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
package com.isolaatti.images.image_maker.ui
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.core.widget.doOnTextChanged
|
|
||||||
import coil.load
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.isolaatti.R
|
|
||||||
import com.isolaatti.common.IsolaattiBaseActivity
|
|
||||||
import com.isolaatti.databinding.ActivityImageMakerBinding
|
|
||||||
import com.isolaatti.images.image_maker.presentation.ImageMakerViewModel
|
|
||||||
import com.isolaatti.utils.Resource
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class ImageMakerActivity : IsolaattiBaseActivity() {
|
|
||||||
private lateinit var binding: ActivityImageMakerBinding
|
|
||||||
private val viewModel: ImageMakerViewModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = ActivityImageMakerBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
viewModel.imageUri = intent.data
|
|
||||||
binding.imagePreview.load(intent.data)
|
|
||||||
onBackPressedDispatcher.addCallback(onBackPressedCallback)
|
|
||||||
setupListeners()
|
|
||||||
setupObservers()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val onBackPressedCallback = object: OnBackPressedCallback(true) {
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
showExitConfirmationDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showExitConfirmationDialog() {
|
|
||||||
MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.discard_image)
|
|
||||||
.setPositiveButton(R.string.yes_discard_image) {_, _ ->
|
|
||||||
finish()
|
|
||||||
}.setNegativeButton(R.string.no, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupListeners() {
|
|
||||||
binding.uploadPhotoFab.setOnClickListener {
|
|
||||||
viewModel.uploadPicture()
|
|
||||||
}
|
|
||||||
binding.toolbar.setNavigationOnClickListener {
|
|
||||||
showExitConfirmationDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupObservers() {
|
|
||||||
viewModel.image.observe(this) {
|
|
||||||
when(it) {
|
|
||||||
is Resource.Error -> {
|
|
||||||
errorViewModel.error.value = it.errorType
|
|
||||||
binding.progressBarLoading.visibility = View.GONE
|
|
||||||
binding.uploadPhotoFab.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
is Resource.Loading -> {
|
|
||||||
binding.progressBarLoading.visibility = View.VISIBLE
|
|
||||||
binding.uploadPhotoFab.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
is Resource.Success -> {
|
|
||||||
binding.progressBarLoading.visibility = View.GONE
|
|
||||||
binding.uploadPhotoFab.visibility = View.VISIBLE
|
|
||||||
setResult(Activity.RESULT_OK, Intent().putExtra(EXTRA_IMAGE, it.data))
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val EXTRA_IMAGE = "image"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
package com.isolaatti.images.image_maker.ui
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
|
||||||
import com.isolaatti.images.common.domain.entity.Image
|
|
||||||
|
|
||||||
class ImageMakerContract : ActivityResultContract<Uri, Image?>() {
|
|
||||||
override fun createIntent(context: Context, input: Uri): Intent {
|
|
||||||
val intent = Intent(context, ImageMakerActivity::class.java)
|
|
||||||
intent.data = input
|
|
||||||
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun parseResult(resultCode: Int, intent: Intent?): Image? {
|
|
||||||
if(resultCode == Activity.RESULT_OK) {
|
|
||||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
intent?.getSerializableExtra(ImageMakerActivity.EXTRA_IMAGE) as Image?
|
|
||||||
} else {
|
|
||||||
intent?.getSerializableExtra(ImageMakerActivity.EXTRA_IMAGE, Image::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package com.isolaatti.posting
|
package com.isolaatti.posting
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
import com.isolaatti.connectivity.RetrofitClient
|
import com.isolaatti.connectivity.RetrofitClient
|
||||||
|
import com.isolaatti.images.common.data.remote.ImagesApi
|
||||||
import com.isolaatti.posting.posts.data.remote.FeedsApi
|
import com.isolaatti.posting.posts.data.remote.FeedsApi
|
||||||
import com.isolaatti.posting.posts.data.remote.PostApi
|
import com.isolaatti.posting.posts.data.remote.PostApi
|
||||||
import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl
|
import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl
|
||||||
@ -25,7 +27,7 @@ class Module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun providePostsRepository(feedsApi: FeedsApi, postApi: PostApi): PostsRepository {
|
fun providePostsRepository(feedsApi: FeedsApi, postApi: PostApi, imagesApi: ImagesApi, contentResolver: ContentResolver): PostsRepository {
|
||||||
return PostsRepositoryImpl(feedsApi, postApi)
|
return PostsRepositoryImpl(feedsApi, postApi, imagesApi, contentResolver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.isolaatti.images.common.components
|
package com.isolaatti.posting.posts.components
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@ -9,15 +9,12 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.FilledIconButton
|
|
||||||
import androidx.compose.material3.FilledTonalIconButton
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -33,26 +30,24 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.tooling.preview.Devices
|
import androidx.compose.ui.tooling.preview.Devices
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Popup
|
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.isolaatti.R
|
import com.isolaatti.R
|
||||||
import com.isolaatti.images.common.domain.entity.Image
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ImagesRow(
|
fun PostAttachments(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
images: List<Uri>,
|
images: List<Uri>,
|
||||||
deletable: Boolean,
|
deletable: Boolean,
|
||||||
addable: Boolean,
|
imageAddable: Boolean,
|
||||||
onClick: (index: Int) -> Unit,
|
onImageClick: (index: Int) -> Unit,
|
||||||
onDeleteClick: (index: Int) -> Unit = {},
|
onImageDeleteClick: (index: Int) -> Unit = {},
|
||||||
onTakePicture: () -> Unit = {},
|
onTakePicture: () -> Unit = {},
|
||||||
onUploadPicture: () -> Unit = {}
|
onUploadPicture: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
var showAddPhotoPopUp by remember { mutableStateOf(false) }
|
var showAddPhotoPopUp by remember { mutableStateOf(false) }
|
||||||
LazyRow(modifier, contentPadding = PaddingValues(horizontal = 8.dp)) {
|
LazyRow(modifier, contentPadding = PaddingValues(horizontal = 8.dp)) {
|
||||||
if(addable) {
|
if(imageAddable) {
|
||||||
item {
|
item {
|
||||||
Card(modifier = Modifier
|
Card(modifier = Modifier
|
||||||
.padding(horizontal = 4.dp)
|
.padding(horizontal = 4.dp)
|
||||||
@ -93,7 +88,7 @@ fun ImagesRow(
|
|||||||
.padding(horizontal = 4.dp)
|
.padding(horizontal = 4.dp)
|
||||||
.size(120.dp),
|
.size(120.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
onClick(index)
|
onImageClick(index)
|
||||||
}) {
|
}) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@ -101,7 +96,7 @@ fun ImagesRow(
|
|||||||
) {
|
) {
|
||||||
if(deletable) {
|
if(deletable) {
|
||||||
FilledTonalIconButton(
|
FilledTonalIconButton(
|
||||||
onClick = { onDeleteClick(index) },
|
onClick = { onImageDeleteClick(index) },
|
||||||
modifier = Modifier.zIndex(2f)
|
modifier = Modifier.zIndex(2f)
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Default.Close, contentDescription = null)
|
Icon(imageVector = Icons.Default.Close, contentDescription = null)
|
||||||
@ -124,5 +119,5 @@ fun ImagesRow(
|
|||||||
@Preview(device = Devices.PIXEL_5)
|
@Preview(device = Devices.PIXEL_5)
|
||||||
@Composable
|
@Composable
|
||||||
fun ImagesRowPreview() {
|
fun ImagesRowPreview() {
|
||||||
ImagesRow(images = emptyList(), deletable = true, addable = true, onClick = {})
|
PostAttachments(images = emptyList(), deletable = true, imageAddable = true, onImageClick = {})
|
||||||
}
|
}
|
||||||
@ -4,5 +4,6 @@ data class CreatePostDto(
|
|||||||
val privacy: Int,
|
val privacy: Int,
|
||||||
val content: String,
|
val content: String,
|
||||||
val audioId: String?,
|
val audioId: String?,
|
||||||
val squadId: String?
|
val squadId: String?,
|
||||||
|
val isDraft: Boolean = false
|
||||||
)
|
)
|
||||||
@ -24,4 +24,7 @@ interface PostApi {
|
|||||||
@GET("Posting/Post/{postId}/Versions")
|
@GET("Posting/Post/{postId}/Versions")
|
||||||
fun getVersions(@Path("postId") postId: Long): Call<ResultDto<List<VersionDto>>>
|
fun getVersions(@Path("postId") postId: Long): Call<ResultDto<List<VersionDto>>>
|
||||||
|
|
||||||
|
@POST("Posting/Post/{postId}/SetIsDraft")
|
||||||
|
fun setIsDraft(@Path("postId") postId: Long, @Query("isDraft") isDraft: Boolean): Call<FeedDto.PostDto>
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,7 +1,16 @@
|
|||||||
package com.isolaatti.posting.posts.data.repository
|
package com.isolaatti.posting.posts.data.repository
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import com.isolaatti.images.common.data.remote.DeleteImagesDto
|
||||||
|
import com.isolaatti.images.common.data.remote.ImageDto
|
||||||
|
import com.isolaatti.images.common.data.remote.ImagesApi
|
||||||
|
import com.isolaatti.images.common.domain.entity.Image
|
||||||
import com.isolaatti.posting.posts.data.remote.CreatePostDto
|
import com.isolaatti.posting.posts.data.remote.CreatePostDto
|
||||||
import com.isolaatti.posting.posts.data.remote.DeletePostDto
|
import com.isolaatti.posting.posts.data.remote.DeletePostDto
|
||||||
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
||||||
@ -17,10 +26,19 @@ import com.isolaatti.profile.domain.entity.ProfileListItem
|
|||||||
import com.isolaatti.utils.Resource
|
import com.isolaatti.utils.Resource
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import retrofit2.awaitResponse
|
import retrofit2.awaitResponse
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, private val postApi: PostApi) : PostsRepository {
|
class PostsRepositoryImpl @Inject constructor(
|
||||||
|
private val feedsApi: FeedsApi,
|
||||||
|
private val postApi: PostApi,
|
||||||
|
private val imagesApi: ImagesApi,
|
||||||
|
private val contentResolver: ContentResolver
|
||||||
|
) : PostsRepository {
|
||||||
companion object {
|
companion object {
|
||||||
const val LOG_TAG = "PostsRepositoryImpl"
|
const val LOG_TAG = "PostsRepositoryImpl"
|
||||||
}
|
}
|
||||||
@ -64,16 +82,91 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, pr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun makePost(createPostDto: CreatePostDto): Flow<Resource<FeedDto.PostDto>> = flow {
|
override fun makePost(createPostDto: CreatePostDto, images: List<Uri>): Flow<Resource<FeedDto.PostDto>> = flow {
|
||||||
emit(Resource.Loading())
|
emit(Resource.Loading())
|
||||||
try {
|
try {
|
||||||
val result = postApi.makePost(createPostDto).awaitResponse()
|
val hasImages = images.isNotEmpty()
|
||||||
|
val result = postApi.makePost(
|
||||||
|
if(hasImages) {
|
||||||
|
createPostDto.copy(isDraft = true)
|
||||||
|
} else {
|
||||||
|
createPostDto
|
||||||
|
}
|
||||||
|
).awaitResponse()
|
||||||
if(result.isSuccessful) {
|
if(result.isSuccessful) {
|
||||||
emit(Resource.Success(result.body()))
|
|
||||||
return@flow
|
if(hasImages) {
|
||||||
|
val successfulImageUploads: MutableList<ImageDto> = mutableListOf()
|
||||||
|
|
||||||
|
val postId = result.body()?.post?.id
|
||||||
|
if(postId == null) {
|
||||||
|
emit(Resource.Error())
|
||||||
|
return@flow
|
||||||
|
}
|
||||||
|
images.forEach { imageUri ->
|
||||||
|
var imageInputStream: InputStream? = null
|
||||||
|
try {
|
||||||
|
imageInputStream = contentResolver.openInputStream(imageUri)
|
||||||
|
val bitmap = BitmapFactory.decodeStream(imageInputStream)
|
||||||
|
|
||||||
|
if(bitmap == null) {
|
||||||
|
Log.e(LOG_TAG, "Error resolving image with content resolver")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
|
||||||
|
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.WEBP, 50, outputStream)
|
||||||
|
} else {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 50, outputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val response = imagesApi.postImage(
|
||||||
|
MultipartBody.Part.createFormData("file", "$postId-${System.currentTimeMillis()}",outputStream.toByteArray().toRequestBody()),
|
||||||
|
MultipartBody.Part.createFormData("postId", postId.toString())
|
||||||
|
).awaitResponse()
|
||||||
|
|
||||||
|
if(response.isSuccessful) {
|
||||||
|
response.body()?.let { successfulImageUploads.add(it) }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e: Exception) {
|
||||||
|
Log.e("ImagesRepository", e.toString())
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
imageInputStream?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// undo post and emit error
|
||||||
|
if(successfulImageUploads.size != images.size) {
|
||||||
|
postApi.deletePost(DeletePostDto(postId)).awaitResponse()
|
||||||
|
emit(Resource.Error(Resource.Error.ErrorType.ServerError, "Some images were not processed correctly"))
|
||||||
|
} else {
|
||||||
|
postApi.setIsDraft(postId, false)
|
||||||
|
val response = postApi.getPost(postId).awaitResponse()
|
||||||
|
|
||||||
|
if(response.isSuccessful) {
|
||||||
|
emit(Resource.Success(response.body()))
|
||||||
|
} else {
|
||||||
|
val msg = "Post posted but could not retrieve updated post data from server"
|
||||||
|
Log.e(LOG_TAG, msg)
|
||||||
|
emit(Resource.Error(Resource.Error.mapErrorCode(response.code()), msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit(Resource.Success(result.body()))
|
||||||
|
return@flow
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
emit(Resource.Error(Resource.Error.mapErrorCode(result.code())))
|
emit(Resource.Error(Resource.Error.mapErrorCode(result.code())))
|
||||||
} catch(_: Exception) {
|
} catch(e: Exception) {
|
||||||
|
Log.d(LOG_TAG, e.toString())
|
||||||
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
|
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.isolaatti.posting.posts.domain
|
package com.isolaatti.posting.posts.domain
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import com.isolaatti.posting.posts.data.remote.CreatePostDto
|
import com.isolaatti.posting.posts.data.remote.CreatePostDto
|
||||||
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
||||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||||
@ -17,7 +18,7 @@ interface PostsRepository {
|
|||||||
|
|
||||||
fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto?): Flow<Resource<MutableList<Post>>>
|
fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto?): Flow<Resource<MutableList<Post>>>
|
||||||
|
|
||||||
fun makePost(createPostDto: CreatePostDto): Flow<Resource<FeedDto.PostDto>>
|
fun makePost(createPostDto: CreatePostDto, images: List<Uri>): Flow<Resource<FeedDto.PostDto>>
|
||||||
fun editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>>
|
fun editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>>
|
||||||
fun deletePost(postId: Long): Flow<Resource<PostDeletedDto>>
|
fun deletePost(postId: Long): Flow<Resource<PostDeletedDto>>
|
||||||
fun loadPost(postId: Long): Flow<Resource<Post>>
|
fun loadPost(postId: Long): Flow<Resource<Post>>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.isolaatti.posting.posts.domain.use_case
|
package com.isolaatti.posting.posts.domain.use_case
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import com.isolaatti.posting.posts.data.remote.CreatePostDto
|
import com.isolaatti.posting.posts.data.remote.CreatePostDto
|
||||||
import com.isolaatti.posting.posts.data.remote.FeedDto
|
import com.isolaatti.posting.posts.data.remote.FeedDto
|
||||||
import com.isolaatti.posting.posts.domain.PostsRepository
|
import com.isolaatti.posting.posts.domain.PostsRepository
|
||||||
@ -11,9 +12,10 @@ class MakePost @Inject constructor(private val postsRepository: PostsRepository)
|
|||||||
operator fun invoke(
|
operator fun invoke(
|
||||||
privacy: Int,
|
privacy: Int,
|
||||||
content: String,
|
content: String,
|
||||||
|
images: List<Uri>,
|
||||||
audioId: String?,
|
audioId: String?,
|
||||||
squadId: String?
|
squadId: String?
|
||||||
): Flow<Resource<FeedDto.PostDto>> {
|
): Flow<Resource<FeedDto.PostDto>> {
|
||||||
return postsRepository.makePost(CreatePostDto(privacy, content, audioId, squadId))
|
return postsRepository.makePost(CreatePostDto(privacy, content, audioId, squadId), images)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,7 +62,13 @@ class CreatePostViewModel @Inject constructor(
|
|||||||
private fun sendDiscussion() {
|
private fun sendDiscussion() {
|
||||||
Log.d(LOG_TAG, "postDiscussion#send()")
|
Log.d(LOG_TAG, "postDiscussion#send()")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
makePost(EditPostDto.PRIVACY_ISOLAATTI, content, audioId, null).onEach {
|
makePost(
|
||||||
|
privacy = EditPostDto.PRIVACY_ISOLAATTI,
|
||||||
|
content = content,
|
||||||
|
images = photos.value,
|
||||||
|
audioId = audioId,
|
||||||
|
squadId = null
|
||||||
|
).onEach {
|
||||||
when(it) {
|
when(it) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
sendingPost.postValue(false)
|
sendingPost.postValue(false)
|
||||||
|
|||||||
@ -1,22 +1,57 @@
|
|||||||
package com.isolaatti.posting.posts.ui
|
package com.isolaatti.posting.posts.ui
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.compose.foundation.gestures.ScrollableState
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.compose.foundation.gestures.rememberScrollableState
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.compose.foundation.gestures.scrollable
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
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.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.isolaatti.MyApplication
|
||||||
import com.isolaatti.R
|
import com.isolaatti.R
|
||||||
|
import com.isolaatti.audio.common.components.AudioRecorder
|
||||||
import com.isolaatti.common.IsolaattiBaseActivity
|
import com.isolaatti.common.IsolaattiBaseActivity
|
||||||
import com.isolaatti.databinding.ActivityCreatePostBinding
|
import com.isolaatti.common.IsolaattiTheme
|
||||||
|
import com.isolaatti.posting.posts.components.PostAttachments
|
||||||
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
|
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import java.io.File
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class CreatePostActivity : IsolaattiBaseActivity() {
|
class CreatePostActivity : IsolaattiBaseActivity() {
|
||||||
@ -36,94 +71,157 @@ class CreatePostActivity : IsolaattiBaseActivity() {
|
|||||||
const val EXTRA_KEY_POST_POSTED = "post"
|
const val EXTRA_KEY_POST_POSTED = "post"
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var binding: ActivityCreatePostBinding
|
|
||||||
val viewModel: CreatePostViewModel by viewModels()
|
val viewModel: CreatePostViewModel by viewModels()
|
||||||
var mode: Int = EXTRA_MODE_CREATE
|
var mode: Int = EXTRA_MODE_CREATE
|
||||||
var postId: Long = 0L
|
var postId: Long = 0L
|
||||||
|
|
||||||
|
private val choosePictureLauncher = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
|
||||||
|
if(it != null) {
|
||||||
|
viewModel.addPicture(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makePhotoUri(): Uri {
|
||||||
|
val cacheFile = File(filesDir, "temp_picture_${Calendar.getInstance().timeInMillis}")
|
||||||
|
return FileProvider.getUriForFile(this, "${MyApplication.myApp.packageName}.provider", cacheFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cameraPhotoUri: Uri? = null
|
||||||
|
|
||||||
|
private val takePhotoLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) {
|
||||||
|
if(it && cameraPhotoUri != null) {
|
||||||
|
viewModel.addPicture(cameraPhotoUri!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
var text by remember { mutableStateOf("") }
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val pictures by viewModel.photos.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val editMode = remember {
|
||||||
|
mode == EXTRA_MODE_EDIT && postId != 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
IsolaattiTheme {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = { Text(stringResource(if(editMode) R.string.edit else R.string.new_post)) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
exit()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Close, null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
Button(
|
||||||
|
enabled = text.isNotBlank() || pictures.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
if(editMode) {
|
||||||
|
viewModel.editDiscussion(postId)
|
||||||
|
} else {
|
||||||
|
viewModel.postDiscussion()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(if(editMode) R.string.save else R.string.post))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
Column(modifier = Modifier.padding(it).verticalScroll(scrollState)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = {
|
||||||
|
text = it
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
placeholder = { Text(stringResource(R.string.what_do_you_want_to_talk_about_you_can_record_an_audio_if_you_want)) },
|
||||||
|
colors = OutlinedTextFieldDefaults
|
||||||
|
.colors(
|
||||||
|
focusedBorderColor = Color.Transparent,
|
||||||
|
unfocusedBorderColor = Color.Transparent
|
||||||
|
),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = {}) {
|
||||||
|
Icon(painterResource(id = R.drawable.baseline_mic_24), null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(false) {
|
||||||
|
AudioRecorder(
|
||||||
|
onAudioRecorded = {
|
||||||
|
|
||||||
|
},
|
||||||
|
dismissible = true,
|
||||||
|
onDismiss = {
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PostAttachments(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
images = pictures,
|
||||||
|
deletable = true,
|
||||||
|
imageAddable = true,
|
||||||
|
onImageClick = {
|
||||||
|
|
||||||
|
},
|
||||||
|
onImageDeleteClick = {
|
||||||
|
viewModel.removePicture(it)
|
||||||
|
},
|
||||||
|
onTakePicture = {
|
||||||
|
cameraPhotoUri = makePhotoUri()
|
||||||
|
takePhotoLauncher.launch(cameraPhotoUri!!)
|
||||||
|
},
|
||||||
|
onUploadPicture = {
|
||||||
|
choosePictureLauncher.launch(
|
||||||
|
PickVisualMediaRequest(
|
||||||
|
ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
intent.extras?.let {
|
intent.extras?.let {
|
||||||
mode = it.getInt(EXTRA_KEY_MODE, EXTRA_MODE_CREATE)
|
mode = it.getInt(EXTRA_KEY_MODE, EXTRA_MODE_CREATE)
|
||||||
postId = it.getLong(EXTRA_KEY_POST_ID)
|
postId = it.getLong(EXTRA_KEY_POST_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
binding = ActivityCreatePostBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
if(mode == EXTRA_MODE_EDIT && postId != 0L) {
|
if(mode == EXTRA_MODE_EDIT && postId != 0L) {
|
||||||
viewModel.loadDiscussion(postId)
|
viewModel.loadDiscussion(postId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setupUI()
|
|
||||||
setListeners()
|
|
||||||
setObservers()
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
errorViewModel.retry.collect {
|
|
||||||
if(!it) {
|
|
||||||
return@collect
|
|
||||||
}
|
|
||||||
|
|
||||||
if(mode == EXTRA_MODE_EDIT && postId != 0L) {
|
|
||||||
viewModel.editDiscussion(postId)
|
|
||||||
} else {
|
|
||||||
viewModel.postDiscussion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupUI() {
|
|
||||||
binding.toolbar.setTitle(if(mode == EXTRA_MODE_EDIT && postId != 0L) R.string.edit else R.string.new_post)
|
|
||||||
|
|
||||||
binding.pager.adapter = CreatePostFragmentStateAdapter(this)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setListeners() {
|
|
||||||
binding.toolbar.setNavigationOnClickListener {
|
|
||||||
exit()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
binding.postButton.setOnClickListener {
|
|
||||||
if(mode == EXTRA_MODE_EDIT && postId != 0L) {
|
|
||||||
viewModel.editDiscussion(postId)
|
|
||||||
} else {
|
|
||||||
viewModel.postDiscussion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setObservers() {
|
|
||||||
viewModel.validation.observe(this@CreatePostActivity) {
|
|
||||||
binding.postButton.isEnabled = it
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.error.observe(this@CreatePostActivity) {
|
|
||||||
errorViewModel.error.postValue(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.posted.observe(this@CreatePostActivity) {
|
|
||||||
setResult(Activity.RESULT_OK, Intent().apply{
|
|
||||||
putExtra(EXTRA_KEY_POST_POSTED, it)
|
|
||||||
})
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.sendingPost.observe(this@CreatePostActivity) {
|
|
||||||
binding.progressBarLoading.visibility = if(it) View.VISIBLE else View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exit() {
|
private fun exit() {
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
package com.isolaatti.posting.posts.ui
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
|
|
||||||
class CreatePostFragmentStateAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
|
||||||
return PostEditingFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
package com.isolaatti.posting.posts.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.widget.doOnTextChanged
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import com.isolaatti.common.CoilImageLoader
|
|
||||||
import com.isolaatti.databinding.FragmentMarkdownEditingBinding
|
|
||||||
import com.isolaatti.databinding.FragmentMarkdownPreviewBinding
|
|
||||||
import com.isolaatti.markdown.HashtagMarkwonPlugin
|
|
||||||
import com.isolaatti.markdown.RelativePathMarkwonPlugin
|
|
||||||
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
|
|
||||||
import dagger.hilt.EntryPoint
|
|
||||||
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
|
|
||||||
|
|
||||||
class MarkdownPreviewFragment : Fragment() {
|
|
||||||
private lateinit var binding: FragmentMarkdownPreviewBinding
|
|
||||||
private val viewModel: CreatePostViewModel by activityViewModels()
|
|
||||||
private var markwon: Markwon? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
markwon = Markwon.builder(requireContext())
|
|
||||||
.usePlugin(RelativePathMarkwonPlugin())
|
|
||||||
.usePlugin(CoilImagesPlugin.create(requireContext(), CoilImageLoader.imageLoader))
|
|
||||||
.usePlugin(LinkifyPlugin.create())
|
|
||||||
.usePlugin(HashtagMarkwonPlugin())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
binding = FragmentMarkdownPreviewBinding.inflate(layoutInflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
|
|
||||||
markwon = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
viewModel.liveContent.observe(viewLifecycleOwner) {
|
|
||||||
markwon?.setMarkdown(binding.textView, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,157 +0,0 @@
|
|||||||
package com.isolaatti.posting.posts.ui
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.result.PickVisualMediaRequest
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import androidx.core.widget.doOnTextChanged
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import com.isolaatti.MyApplication
|
|
||||||
import com.isolaatti.R
|
|
||||||
import com.isolaatti.audio.common.domain.Audio
|
|
||||||
import com.isolaatti.audio.common.domain.Playable
|
|
||||||
import com.isolaatti.common.IsolaattiTheme
|
|
||||||
import com.isolaatti.databinding.FragmentMarkdownEditingBinding
|
|
||||||
import com.isolaatti.images.common.components.ImagesRow
|
|
||||||
import com.isolaatti.posting.link_creator.presentation.LinkCreatorViewModel
|
|
||||||
import com.isolaatti.posting.link_creator.ui.LinkCreatorFragment
|
|
||||||
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
|
|
||||||
import java.io.File
|
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
class PostEditingFragment : Fragment(){
|
|
||||||
companion object {
|
|
||||||
const val LOG_TAG = "PostEditingFragment"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private lateinit var binding: FragmentMarkdownEditingBinding
|
|
||||||
private val viewModel: CreatePostViewModel by activityViewModels()
|
|
||||||
private val linkCreatorViewModel: LinkCreatorViewModel by viewModels()
|
|
||||||
|
|
||||||
|
|
||||||
private val choosePictureLauncher = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
|
|
||||||
if(it != null) {
|
|
||||||
viewModel.addPicture(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun makePhotoUri(): Uri {
|
|
||||||
val cacheFile = File(requireContext().filesDir, "temp_picture_${Calendar.getInstance().timeInMillis}")
|
|
||||||
return FileProvider.getUriForFile(requireContext(), "${MyApplication.myApp.packageName}.provider", cacheFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cameraPhotoUri: Uri? = null
|
|
||||||
|
|
||||||
private val takePhotoLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) {
|
|
||||||
if(it && cameraPhotoUri != null) {
|
|
||||||
viewModel.addPicture(cameraPhotoUri!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
|
|
||||||
binding = FragmentMarkdownEditingBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycle.addObserver(object: LifecycleEventObserver {
|
|
||||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
|
||||||
Log.d(LOG_TAG, event.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setupListeners()
|
|
||||||
setupObservers()
|
|
||||||
|
|
||||||
binding.composeView.apply {
|
|
||||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
||||||
setContent {
|
|
||||||
val pictures by viewModel.photos.collectAsState()
|
|
||||||
IsolaattiTheme {
|
|
||||||
ImagesRow(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
images = pictures,
|
|
||||||
deletable = true,
|
|
||||||
addable = true,
|
|
||||||
onClick = {
|
|
||||||
|
|
||||||
},
|
|
||||||
onDeleteClick = {
|
|
||||||
viewModel.removePicture(it)
|
|
||||||
},
|
|
||||||
onTakePicture = {
|
|
||||||
cameraPhotoUri = makePhotoUri()
|
|
||||||
takePhotoLauncher.launch(cameraPhotoUri)
|
|
||||||
},
|
|
||||||
onUploadPicture = {
|
|
||||||
choosePictureLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupListeners() {
|
|
||||||
binding.filledTextField.editText?.setText(viewModel.content)
|
|
||||||
binding.filledTextField.requestFocus()
|
|
||||||
binding.filledTextField.editText?.doOnTextChanged { text, _, _, _ ->
|
|
||||||
// make better validation :)
|
|
||||||
viewModel.validation.postValue(!text.isNullOrEmpty())
|
|
||||||
viewModel.content = text.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupObservers(){
|
|
||||||
viewModel.postToEdit.observe(viewLifecycleOwner) {
|
|
||||||
binding.filledTextField.editText?.setText(it.content)
|
|
||||||
}
|
|
||||||
linkCreatorViewModel.inserted.observe(viewLifecycleOwner) {
|
|
||||||
if(it) {
|
|
||||||
viewModel.content += " ${linkCreatorViewModel.markdown}"
|
|
||||||
binding.filledTextField.editText?.setText(viewModel.content)
|
|
||||||
linkCreatorViewModel.inserted.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.audioAttachment.observe(viewLifecycleOwner) { playable ->
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,66 +5,55 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appBarLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:fitsSystemWindows="true">
|
android:fitsSystemWindows="true">
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/appBarLayout"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:navigationIcon="@drawable/baseline_close_24"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:navigationIconTint="@color/on_surface"
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
app:title="@string/new_post"
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
app:titleCentered="true">
|
||||||
android:id="@+id/toolbar"
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingBottom="100dp"
|
||||||
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
style="?attr/textInputFilledStyle"
|
||||||
|
android:id="@+id/filledTextField"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
app:boxBackgroundMode="none"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:navigationIcon="@drawable/baseline_close_24"
|
app:hintEnabled="false">
|
||||||
app:navigationIconTint="@color/on_surface"
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
app:title="@string/new_post"
|
android:layout_width="match_parent"
|
||||||
app:titleCentered="true">
|
android:layout_height="wrap_content"
|
||||||
</com.google.android.material.appbar.MaterialToolbar>
|
android:hint="@string/what_do_you_want_to_talk_about_you_can_record_an_audio_if_you_want"/>
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
<androidx.compose.ui.platform.ComposeView
|
||||||
|
android:id="@+id/compose_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="160dp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
</androidx.core.widget.NestedScrollView>
|
||||||
android:id="@+id/pager"
|
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/appBarLayout" />
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/progress_bar_loading"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/postButton"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/postButton"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/postButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:srcCompat="@drawable/baseline_send_24"
|
|
||||||
android:fitsSystemWindows="true"/>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:paddingBottom="100dp"
|
|
||||||
android:layout_marginTop="?attr/actionBarSize"
|
|
||||||
android:clipToPadding="false">
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
style="?attr/textInputFilledStyle"
|
|
||||||
android:id="@+id/filledTextField"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
app:boxBackgroundMode="none"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:hintEnabled="false">
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/what_do_you_want_to_talk_about_you_can_record_an_audio_if_you_want"/>
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<androidx.compose.ui.platform.ComposeView
|
|
||||||
android:id="@+id/compose_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="160dp"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user