WIP subir foto

This commit is contained in:
Erik Everardo 2023-11-23 00:10:57 -06:00
parent 1cca08df0b
commit 5992f2a07a
27 changed files with 246 additions and 76 deletions

View File

@ -1,12 +1,17 @@
package com.isolaatti.images
import android.app.Application
import android.content.ContentResolver
import android.content.Context
import com.isolaatti.MyApplication
import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.images.image_list.data.remote.ImagesApi
import com.isolaatti.images.image_list.data.repository.ImagesRepositoryImpl
import com.isolaatti.images.image_list.domain.repository.ImagesRepository
import com.isolaatti.images.common.data.remote.ImagesApi
import com.isolaatti.images.common.data.repository.ImagesRepositoryImpl
import com.isolaatti.images.common.domain.repository.ImagesRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@ -18,7 +23,12 @@ class Module {
}
@Provides
fun provideImagesRepository(imagesApi: ImagesApi): ImagesRepository {
return ImagesRepositoryImpl(imagesApi)
fun provideContentResolver(@ApplicationContext application: Context): ContentResolver {
return application.contentResolver
}
@Provides
fun provideImagesRepository(imagesApi: ImagesApi, contentResolver: ContentResolver): ImagesRepository {
return ImagesRepositoryImpl(imagesApi, contentResolver)
}
}

View File

@ -1,4 +1,4 @@
package com.isolaatti.images.image_list.data.remote
package com.isolaatti.images.common.data.remote
data class DeleteImagesDto(
val imageIds: List<String>

View File

@ -1,4 +1,4 @@
package com.isolaatti.images.image_list.data.remote
package com.isolaatti.images.common.data.remote
data class DeleteImagesResultDto(
val success: Boolean,

View File

@ -1,4 +1,4 @@
package com.isolaatti.images.image_list.data.remote
package com.isolaatti.images.common.data.remote
data class ImageDto(
val id: String,

View File

@ -1,6 +1,7 @@
package com.isolaatti.images.image_list.data.remote
package com.isolaatti.images.common.data.remote
import com.isolaatti.utils.SimpleData
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.http.Body
@ -19,7 +20,7 @@ interface ImagesApi {
@POST("images/create")
@Multipart
fun postImage(@Part("file") file: RequestBody,
fun postImage(@Part file: MultipartBody.Part,
@Part("name") name: String,
@Part("setAsProfile") setAsProfile: Boolean? = null,
@Part("squadId") squadId: String? = null): Call<ImageDto>

View File

@ -0,0 +1,5 @@
package com.isolaatti.images.common.data.remote
data class ImagesDto(
val data: List<ImageDto>
)

View File

@ -0,0 +1,76 @@
package com.isolaatti.images.common.data.repository
import android.content.ContentResolver
import android.net.Uri
import android.util.Log
import com.isolaatti.images.common.data.remote.ImagesApi
import com.isolaatti.images.common.domain.entity.Image
import com.isolaatti.images.common.domain.repository.ImagesRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.awaitResponse
import java.io.InputStream
import javax.inject.Inject
class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi, private val contentResolver: ContentResolver) :
ImagesRepository {
override fun getImagesOfUser(userId: Int, lastId: String?): Flow<Resource<List<Image>>> = flow {
emit(Resource.Loading())
try {
val response = imagesApi.getImagesOfUser(userId, lastId).awaitResponse()
if(response.isSuccessful) {
val imagesDto = response.body()
val images = imagesDto?.data?.map { Image.fromDto(it) }
emit(Resource.Success(images))
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
override fun deleteImages(images: List<Image>): Flow<Resource<Boolean>> = flow {
}
override fun uploadImage(name: String, imageUri: Uri, squadId: String?): Flow<Resource<Image>> = flow {
var imageInputStream: InputStream? = null
try {
imageInputStream = contentResolver.openInputStream(imageUri)
val imageBytes = imageInputStream?.readBytes()
if(imageBytes == null) {
emit(Resource.Error(Resource.Error.ErrorType.InputError))
return@flow
}
Log.d("ImagesRepository", "${imageBytes.size} bytes")
val response = imagesApi.postImage(MultipartBody.Part.createFormData("file", name,imageBytes.toRequestBody()), name).awaitResponse()
if(response.isSuccessful) {
val imageDto = response.body()
if(imageDto == null) {
emit(Resource.Error(Resource.Error.ErrorType.ServerError))
return@flow
}
val image = Image.fromDto(imageDto)
emit(Resource.Success(image))
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch(e: Exception) {
Log.e("ImagesRepository", e.toString())
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
} finally {
imageInputStream?.close()
}
}
}

View File

@ -1,6 +1,6 @@
package com.isolaatti.images.image_list.domain.entity
package com.isolaatti.images.common.domain.entity
import com.isolaatti.images.image_list.data.remote.ImageDto
import com.isolaatti.images.common.data.remote.ImageDto
import com.isolaatti.utils.UrlGen
import java.io.Serializable

View File

@ -1,10 +1,12 @@
package com.isolaatti.images.image_list.domain.repository
package com.isolaatti.images.common.domain.repository
import com.isolaatti.images.image_list.domain.entity.Image
import android.net.Uri
import com.isolaatti.images.common.domain.entity.Image
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
interface ImagesRepository {
fun getImagesOfUser(userId: Int, lastId: String? = null): Flow<Resource<List<Image>>>
fun deleteImages(images: List<Image>): Flow<Resource<Boolean>>
fun uploadImage(name: String, imageUri: Uri, squadId: String?): Flow<Resource<Image>>
}

View File

@ -1,5 +0,0 @@
package com.isolaatti.images.image_list.data.remote
data class ImagesDto(
val data: List<ImageDto>
)

View File

@ -1,33 +0,0 @@
package com.isolaatti.images.image_list.data.repository
import com.isolaatti.images.image_list.data.remote.ImagesApi
import com.isolaatti.images.image_list.domain.entity.Image
import com.isolaatti.images.image_list.domain.repository.ImagesRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
import javax.inject.Inject
class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi) : ImagesRepository {
override fun getImagesOfUser(userId: Int, lastId: String?): Flow<Resource<List<Image>>> = flow {
try {
val response = imagesApi.getImagesOfUser(userId, lastId).awaitResponse()
if(response.isSuccessful) {
val imagesDto = response.body()
val images = imagesDto?.data?.map { Image.fromDto(it) }
emit(Resource.Success(images))
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
override fun deleteImages(images: List<Image>): Flow<Resource<Boolean>> = flow {
}
}

View File

@ -3,8 +3,8 @@ package com.isolaatti.images.image_list.presentation
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.images.image_list.domain.entity.Image
import com.isolaatti.images.image_list.domain.repository.ImagesRepository
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

View File

@ -9,7 +9,7 @@ import androidx.recyclerview.widget.RecyclerView.Adapter
import coil.load
import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.databinding.ImageItemBinding
import com.isolaatti.images.image_list.domain.entity.Image
import com.isolaatti.images.common.domain.entity.Image
class ImagesAdapter(
private val imageOnClick: ((images: List<Image>, position: Int) -> Unit),

View File

@ -9,6 +9,7 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu
@ -22,7 +23,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.isolaatti.MyApplication
import com.isolaatti.R
import com.isolaatti.databinding.FragmentImagesBinding
import com.isolaatti.images.image_list.domain.entity.Image
import com.isolaatti.images.common.domain.entity.Image
import com.isolaatti.images.image_list.presentation.ImageListViewModel
import com.isolaatti.images.image_list.presentation.ImagesAdapter
import com.isolaatti.images.image_maker.ui.ImageMakerContract
@ -46,7 +47,7 @@ class ImagesFragment : Fragment() {
}
private val imageMakerLauncher = registerForActivityResult(ImageMakerContract()) {
Toast.makeText(requireContext(), "se subio la imagen ${it?.id}", Toast.LENGTH_SHORT).show()
}
private val choosePictureLauncher = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
@ -191,9 +192,12 @@ class ImagesFragment : Fragment() {
viewModel.list.observe(viewLifecycleOwner) { resource ->
when(resource) {
is Resource.Error -> {}
is Resource.Loading -> {}
is Resource.Loading -> {
viewBinding.progressBarLoading.visibility = View.VISIBLE
}
is Resource.Success -> {
resource.data?.let {
viewBinding.progressBarLoading.visibility = View.GONE
adapter.setData(it)
}
}

View File

@ -1,9 +1,33 @@
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() : ViewModel() {
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!!, imageUri!!, null).onEach {
image.postValue(it)
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
}

View File

@ -1,21 +1,73 @@
package com.isolaatti.images.image_maker.ui
import android.app.Activity
import android.content.ContentProvider
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.content.ContentProviderCompat
import androidx.core.widget.doOnTextChanged
import coil.load
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)
setupListeners()
setupObservers()
}
private fun setupListeners() {
binding.materialToolbar.setOnMenuItemClickListener {
when(it.itemId) {
R.id.upload_picture_item -> {
viewModel.uploadPicture()
true
}
else -> false
}
}
binding.textImageName.editText?.doOnTextChanged { text, _, _, _ ->
viewModel.name = text.toString()
}
}
private fun setupObservers() {
viewModel.image.observe(this) {
when(it) {
is Resource.Error -> {
errorViewModel.error.value = it.errorType
}
is Resource.Loading -> {
binding.progressBarLoading.visibility = View.VISIBLE
}
is Resource.Success -> {
binding.progressBarLoading.visibility = View.GONE
setResult(Activity.RESULT_OK)
intent = Intent().putExtra(EXTRA_IMAGE, it.data)
finish()
}
}
}
}
companion object {
const val EXTRA_IMAGE = "image"
}
}

View File

@ -1,10 +1,12 @@
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.image_list.domain.entity.Image
import com.isolaatti.images.common.domain.entity.Image
class ImageMakerContract : ActivityResultContract<Uri, Image?>() {
override fun createIntent(context: Context, input: Uri): Intent {
@ -15,6 +17,13 @@ class ImageMakerContract : ActivityResultContract<Uri, Image?>() {
}
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
}
}

View File

@ -2,7 +2,7 @@ package com.isolaatti.images.picture_viewer.presentation
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.isolaatti.images.image_list.domain.entity.Image
import com.isolaatti.images.common.domain.entity.Image
import com.isolaatti.images.picture_viewer.ui.PictureViewerImageWrapperFragment
class PictureViewerViewPagerAdapter(fragment: Fragment, private val images: Array<Image>) : FragmentStateAdapter(fragment) {

View File

@ -5,7 +5,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.isolaatti.databinding.ActivityPictureViewerBinding
import com.isolaatti.images.image_list.domain.entity.Image
import com.isolaatti.images.common.domain.entity.Image
import com.isolaatti.images.picture_viewer.presentation.PictureViewerViewPagerAdapter
class PictureViewerActivity : AppCompatActivity() {

View File

@ -8,7 +8,7 @@ import androidx.fragment.app.Fragment
import coil.load
import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.databinding.FragmentTouchImageViewWrapperBinding
import com.isolaatti.images.image_list.domain.entity.Image
import com.isolaatti.images.common.domain.entity.Image
import com.ortiz.touchview.OnTouchImageViewListener

View File

@ -7,7 +7,7 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewpager2.widget.ViewPager2
import com.isolaatti.databinding.FragmentMainPictureViewerBinding
import com.isolaatti.images.image_list.domain.entity.Image
import com.isolaatti.images.common.domain.entity.Image
import com.isolaatti.images.picture_viewer.presentation.PictureViewerViewPagerAdapter
class PictureViewerMainFragment : Fragment() {

View File

@ -60,6 +60,7 @@ class LogInActivity: AppCompatActivity() {
Resource.Error.ErrorType.ServerError -> showServerErrorMessage()
Resource.Error.ErrorType.OtherError -> showUnknownErrorMessage()
null -> {}
Resource.Error.ErrorType.InputError -> {}
}
}

View File

@ -5,7 +5,7 @@ sealed class Resource<T> {
class Loading<T>: Resource<T>()
class Error<T>(val errorType: ErrorType? = null, val message: String? = null): Resource<T>() {
enum class ErrorType {
NetworkError, AuthError, NotFoundError, ServerError, OtherError
NetworkError, AuthError, NotFoundError, ServerError, InputError, OtherError
}
companion object {
fun mapErrorCode(errorCode: Int): ErrorType {

View File

@ -13,7 +13,8 @@
app:title="@string/upload_a_picture"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/image_maker_menu"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textImageName"
@ -24,6 +25,7 @@
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:hint="@string/picture_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/materialToolbar">
@ -39,20 +41,21 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="32dp"
app:layout_constraintBottom_toTopOf="@+id/postButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textImageName" />
<com.google.android.material.button.MaterialButton
android:id="@+id/postButton"
android:layout_width="0dp"
<ProgressBar
android:id="@+id/progress_bar_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/upload_photo"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/materialToolbar"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
@ -22,6 +23,13 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
<ProgressBar
android:id="@+id/progress_bar_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/new_picture_button"

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/upload_picture_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/baseline_check_24"
android:title="@string/upload_photo"
app:showAsAction="always"/>
</menu>

View File

@ -120,4 +120,5 @@
<string name="upload_photo">Upload picture</string>
<string name="delete_images_dialog_message">Remove %d images?</string>
<string name="selected_images_count">Images selected: %d</string>
<string name="picture_name">Picture name</string>
</resources>