WIP eliminar fotos

This commit is contained in:
Erik Everardo 2023-11-22 22:02:39 -06:00
parent dacba723b0
commit 1cca08df0b
18 changed files with 291 additions and 11 deletions

View File

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

View File

@ -0,0 +1,6 @@
package com.isolaatti.images.image_list.data.remote
data class DeleteImagesResultDto(
val success: Boolean,
val unSuccessIds: List<String>
)

View File

@ -27,6 +27,9 @@ interface ImagesApi {
@DELETE("images/{imageId}") @DELETE("images/{imageId}")
fun deleteImage(@Path("imageId") imageId: String): Call<Any> fun deleteImage(@Path("imageId") imageId: String): Call<Any>
@DELETE("images/delete_many")
fun deleteImages(@Body deleteImagesDto: DeleteImagesDto): Call<DeleteImagesResultDto>
@GET("images/of_squad/{squadId}") @GET("images/of_squad/{squadId}")
fun getImagesOfSquad(@Path("squadId") squadId: String, fun getImagesOfSquad(@Path("squadId") squadId: String,
@Query("lastId") lastId: String?): Call<List<ImageDto>> @Query("lastId") lastId: String?): Call<List<ImageDto>>

View File

@ -26,4 +26,8 @@ class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi)
emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
} }
} }
override fun deleteImages(images: List<Image>): Flow<Resource<Boolean>> = flow {
}
} }

View File

@ -6,4 +6,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>>
} }

View File

@ -26,4 +26,8 @@ class ImageListViewModel @Inject constructor(private val imagesRepository: Image
}.flowOn(Dispatchers.IO).launchIn(this) }.flowOn(Dispatchers.IO).launchIn(this)
} }
} }
fun removeImages(images: List<Image>) {
}
} }

View File

@ -1,6 +1,7 @@
package com.isolaatti.images.image_list.presentation package com.isolaatti.images.image_list.presentation
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -10,17 +11,36 @@ import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.databinding.ImageItemBinding import com.isolaatti.databinding.ImageItemBinding
import com.isolaatti.images.image_list.domain.entity.Image import com.isolaatti.images.image_list.domain.entity.Image
class ImagesAdapter(private val imageOnClick: ((images: List<Image>, position: Int) -> Unit), private val itemWidth: Int) : Adapter<ImagesAdapter.ImageViewHolder>(){ class ImagesAdapter(
private val imageOnClick: ((images: List<Image>, position: Int) -> Unit),
private val itemWidth: Int,
private val onImageSelectedCountUpdate: ((count: Int) -> Unit)? = null,
private val onDeleteMode: ((enabled: Boolean) -> Unit)? = null) : Adapter<ImagesAdapter.ImageViewHolder>(){
private var data: List<Image> = listOf() private var data: List<Image> = listOf()
private var selectionState: Array<Boolean> = arrayOf()
var deleteMode: Boolean = false
set(value) {
field = value
if(!value) {
selectionState.forEachIndexed { index, _ -> selectionState[index] = false }
}
notifyDataSetChanged()
}
inner class ImageViewHolder(val imageItemBinding: ImageItemBinding) : RecyclerView.ViewHolder(imageItemBinding.root) inner class ImageViewHolder(val imageItemBinding: ImageItemBinding) : RecyclerView.ViewHolder(imageItemBinding.root)
fun setData(data: List<Image>) { fun setData(data: List<Image>) {
this.data = data this.data = data
selectionState = Array(data.size) { false }
notifyDataSetChanged() notifyDataSetChanged()
} }
fun getSelectedImages(): List<Image> {
return data.filterIndexed { index, _ -> selectionState[index] }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
val binding = ImageItemBinding.inflate(inflater) val binding = ImageItemBinding.inflate(inflater)
@ -36,9 +56,35 @@ class ImagesAdapter(private val imageOnClick: ((images: List<Image>, position: I
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val image = data[position] val image = data[position]
holder.imageItemBinding.image.load(image.smallImageUrl, imageLoader) holder.imageItemBinding.image.load(image.reducedImageUrl, imageLoader)
if(deleteMode) {
holder.imageItemBinding.imageCheckbox.visibility = View.VISIBLE
holder.imageItemBinding.imageOverlay.visibility = View.VISIBLE
holder.imageItemBinding.root.setOnClickListener {
holder.imageItemBinding.imageCheckbox.isChecked = !holder.imageItemBinding.imageCheckbox.isChecked
}
holder.imageItemBinding.imageCheckbox.isChecked = selectionState[position]
holder.imageItemBinding.imageCheckbox.setOnCheckedChangeListener { buttonView, isChecked ->
selectionState[position] = isChecked
onImageSelectedCountUpdate?.invoke(selectionState.count { it })
}
holder.imageItemBinding.root.setOnLongClickListener(null)
} else {
holder.imageItemBinding.imageCheckbox.visibility = View.GONE
holder.imageItemBinding.imageCheckbox.isChecked = false
holder.imageItemBinding.imageOverlay.visibility = View.GONE
holder.imageItemBinding.root.setOnClickListener { holder.imageItemBinding.root.setOnClickListener {
imageOnClick(data, position) imageOnClick(data, position)
} }
holder.imageItemBinding.imageCheckbox.setOnCheckedChangeListener(null)
holder.imageItemBinding.root.setOnLongClickListener {
selectionState[position] = true
onDeleteMode?.invoke(true)
onImageSelectedCountUpdate?.invoke(selectionState.count { it })
true
}
}
} }
} }

View File

@ -3,7 +3,10 @@ package com.isolaatti.images.image_list.ui
import android.content.res.Resources import android.content.res.Resources
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.ActionMode
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.PickVisualMediaRequest
@ -15,6 +18,7 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.isolaatti.MyApplication import com.isolaatti.MyApplication
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.FragmentImagesBinding import com.isolaatti.databinding.FragmentImagesBinding
@ -88,10 +92,60 @@ class ImagesFragment : Fragment() {
viewBinding.topAppBar.inflateMenu(R.menu.images_menu) viewBinding.topAppBar.inflateMenu(R.menu.images_menu)
} }
private fun showDeleteDialog() {
val imagesToDelete = adapter.getSelectedImages()
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.delete)
.setMessage(getString(R.string.delete_images_dialog_message, imagesToDelete.size))
.setPositiveButton(R.string.yes_continue) { _, _ ->
viewModel.removeImages(imagesToDelete)
}
.setNegativeButton(R.string.no, null)
.show()
}
private var actionMode: ActionMode? = null
private val contextBarCallback: ActionMode.Callback = object: ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
requireActivity().menuInflater.inflate(R.menu.images_context_menu, menu)
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return true
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return when(item?.itemId) {
R.id.delete_item -> {
showDeleteDialog()
true
}
else -> false
}
}
override fun onDestroyActionMode(mode: ActionMode?) {
adapter.deleteMode = false
}
}
private fun setupListeners() { private fun setupListeners() {
viewBinding.topAppBar.setNavigationOnClickListener { viewBinding.topAppBar.setNavigationOnClickListener {
findNavController().popBackStack() findNavController().popBackStack()
} }
viewBinding.topAppBar.setOnMenuItemClickListener {
when(it.itemId) {
R.id.delete_mode_item -> {
adapter.deleteMode = true
actionMode = requireActivity().startActionMode(contextBarCallback)
true
}
else -> false
}
}
viewBinding.newPictureButton.setOnClickListener { viewBinding.newPictureButton.setOnClickListener {
val popup = PopupMenu(requireContext(), it) val popup = PopupMenu(requireContext(), it)
popup.menuInflater.inflate(R.menu.add_picture_menu, popup.menu) popup.menuInflater.inflate(R.menu.add_picture_menu, popup.menu)
@ -116,7 +170,17 @@ class ImagesFragment : Fragment() {
} }
private fun setupAdapter() { private fun setupAdapter() {
adapter = ImagesAdapter(imageOnClick, Resources.getSystem().displayMetrics.widthPixels/3) adapter = ImagesAdapter(
imageOnClick = imageOnClick,
itemWidth = Resources.getSystem().displayMetrics.widthPixels/3,
onImageSelectedCountUpdate = {
actionMode?.title = getString(R.string.selected_images_count, it)
actionMode?.menu?.findItem(R.id.delete_item)?.isEnabled = it > 0
},
onDeleteMode = {
adapter.deleteMode = it
actionMode = requireActivity().startActionMode(contextBarCallback)
})
viewBinding.recyclerView.layoutManager = viewBinding.recyclerView.layoutManager =
GridLayoutManager(requireContext(), 3, GridLayoutManager.VERTICAL, false) GridLayoutManager(requireContext(), 3, GridLayoutManager.VERTICAL, false)
viewBinding.recyclerView.adapter = adapter viewBinding.recyclerView.adapter = adapter

View File

@ -1,4 +1,4 @@
<vector android:height="24dp" android:tint="#000000" <vector android:height="24dp" android:tint="@color/on_surface"
android:viewportHeight="24" android:viewportWidth="24" android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> <path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>

View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:translationZ="10dp"
android:visibility="gone" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.card.MaterialCardView
android:layout_width="400dp"
android:layout_height="wrap_content"
android:layout_gravity="center">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:text="@string/app_name"
android:textAlignment="center"
style="@style/toolbar_text" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
android:layout_margin="24dp"
android:text="@string/sign_in"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textFieldEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
android:hint="@string/email">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textFieldPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
app:endIconMode="password_toggle"
android:hint="@string/password">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
/>
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/sign_in_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sign_in"
android:layout_margin="8dp"/>
<Button
android:id="@+id/forgot_password_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_gravity="center_horizontal"
android:text="@string/forgot_password"
style="@style/Widget.Material3.Button.TextButton"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/or_you_can_sign_up"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp"/>
<Button
android:id="@+id/sign_up_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp"
android:text="@string/sign_up"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</ScrollView>
</FrameLayout>

View File

@ -4,6 +4,7 @@
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">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar

View File

@ -1,10 +1,25 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ImageView <ImageView
android:id="@+id/image" android:id="@+id/image"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"
android:src="@drawable/baseline_image_24"/>
<CheckBox
android:id="@+id/image_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"/>
<FrameLayout
android:id="@+id/image_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="@color/translucent_purple"
tools:visibility="visible"/>
</LinearLayout> </RelativeLayout>

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/delete_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:showAsAction="always"
android:icon="@drawable/baseline_delete_24"
android:title="@string/delete" />
</menu>

View File

@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/delete_item" android:id="@+id/delete_mode_item"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:icon="@drawable/baseline_delete_24" android:icon="@drawable/baseline_delete_24"

View File

@ -5,6 +5,8 @@
<item name="android:colorPrimary">@color/purple</item> <item name="android:colorPrimary">@color/purple</item>
<item name="android:statusBarColor">@color/purple</item> <item name="android:statusBarColor">@color/purple</item>
<item name="colorOnSurface">@color/on_surface</item> <item name="colorOnSurface">@color/on_surface</item>
<item name="windowActionModeOverlay">true</item>
<item name="actionModeCloseDrawable">@drawable/baseline_close_24</item>
<item name="actionBarTheme">@style/ThemeOverlay.Material3.Dark.ActionBar</item>
</style> </style>
</resources> </resources>

View File

@ -7,4 +7,5 @@
<color name="danger">#BA0606</color> <color name="danger">#BA0606</color>
<color name="black">#000000</color> <color name="black">#000000</color>
<color name="translucent_black">#9A000000</color> <color name="translucent_black">#9A000000</color>
<color name="translucent_purple">#704D3B68</color>
</resources> </resources>

View File

@ -118,4 +118,6 @@
<string name="upload_a_picture">Upload a picture</string> <string name="upload_a_picture">Upload a picture</string>
<string name="take_a_photo">Take a photo</string> <string name="take_a_photo">Take a photo</string>
<string name="upload_photo">Upload picture</string> <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>
</resources> </resources>

View File

@ -5,6 +5,9 @@
<item name="colorSurface">@color/surface</item> <item name="colorSurface">@color/surface</item>
<item name="colorOnSurface">@color/on_surface</item> <item name="colorOnSurface">@color/on_surface</item>
<item name="android:statusBarColor">@color/purple</item> <item name="android:statusBarColor">@color/purple</item>
<item name="windowActionModeOverlay">true</item>
<item name="actionModeCloseDrawable">@drawable/baseline_close_24</item>
<item name="actionBarTheme">@style/ThemeOverlay.Material3.Dark.ActionBar</item>
</style> </style>
<style name="Theme.Isolaatti.Splash" parent="Theme.SplashScreen"/> <style name="Theme.Isolaatti.Splash" parent="Theme.SplashScreen"/>