added helper class for bulk delete and mad pets have bulk delete
This commit is contained in:
@@ -10,16 +10,16 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
|
|
||||||
import com.example.petstoremobile.databinding.ItemInventoryBinding;
|
import com.example.petstoremobile.databinding.ItemInventoryBinding;
|
||||||
import com.example.petstoremobile.dtos.InventoryDTO;
|
import com.example.petstoremobile.dtos.InventoryDTO;
|
||||||
|
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||||
|
import com.example.petstoremobile.utils.SelectionHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.InventoryViewHolder> {
|
public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.InventoryViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||||
|
|
||||||
private final List<InventoryDTO> inventoryList;
|
private final List<InventoryDTO> inventoryList;
|
||||||
private final OnInventoryClickListener clickListener;
|
private final OnInventoryClickListener clickListener;
|
||||||
private final List<Long> selectedIds = new ArrayList<>();
|
private final SelectionHelper selectionHelper;
|
||||||
private boolean selectionMode = false;
|
|
||||||
|
|
||||||
public interface OnInventoryClickListener {
|
public interface OnInventoryClickListener {
|
||||||
void onInventoryClick(int position);
|
void onInventoryClick(int position);
|
||||||
@@ -30,6 +30,27 @@ public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.Inve
|
|||||||
public InventoryAdapter(List<InventoryDTO> inventoryList, OnInventoryClickListener clickListener) {
|
public InventoryAdapter(List<InventoryDTO> inventoryList, OnInventoryClickListener clickListener) {
|
||||||
this.inventoryList = inventoryList;
|
this.inventoryList = inventoryList;
|
||||||
this.clickListener = clickListener;
|
this.clickListener = clickListener;
|
||||||
|
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||||
|
@Override
|
||||||
|
public void onSelectionChanged(int count) {
|
||||||
|
clickListener.onSelectionChanged(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSelectionModeToggle(boolean selectionMode) {
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Long> getSelectedIds() {
|
||||||
|
return selectionHelper.getSelectedIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearSelection() {
|
||||||
|
selectionHelper.clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class InventoryViewHolder extends RecyclerView.ViewHolder {
|
public static class InventoryViewHolder extends RecyclerView.ViewHolder {
|
||||||
@@ -71,64 +92,31 @@ public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.Inve
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bulk delete selection mode
|
// Bulk delete selection mode
|
||||||
if (selectionMode) {
|
if (selectionHelper.isInSelectionMode()) {
|
||||||
binding.cbSelectInventory.setVisibility(View.VISIBLE);
|
binding.cbSelectInventory.setVisibility(View.VISIBLE);
|
||||||
binding.cbSelectInventory.setChecked(inv.getInventoryId() != null
|
binding.cbSelectInventory.setChecked(selectionHelper.isSelected(inv.getInventoryId()));
|
||||||
&& selectedIds.contains(inv.getInventoryId()));
|
|
||||||
} else {
|
} else {
|
||||||
binding.cbSelectInventory.setVisibility(View.GONE);
|
binding.cbSelectInventory.setVisibility(View.GONE);
|
||||||
binding.cbSelectInventory.setChecked(false);
|
binding.cbSelectInventory.setChecked(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemView.setOnClickListener(v -> {
|
holder.itemView.setOnClickListener(v -> {
|
||||||
if (selectionMode) {
|
if (selectionHelper.isInSelectionMode()) {
|
||||||
toggleSelection(inv.getInventoryId(), binding.cbSelectInventory);
|
selectionHelper.toggleSelection(inv.getInventoryId());
|
||||||
|
notifyItemChanged(position);
|
||||||
} else {
|
} else {
|
||||||
clickListener.onInventoryClick(holder.getAdapterPosition());
|
clickListener.onInventoryClick(holder.getAdapterPosition());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
holder.itemView.setOnLongClickListener(v -> {
|
holder.itemView.setOnLongClickListener(v -> {
|
||||||
if (!selectionMode) {
|
if (!selectionHelper.isInSelectionMode()) {
|
||||||
selectionMode = true;
|
selectionHelper.startSelection(inv.getInventoryId());
|
||||||
toggleSelection(inv.getInventoryId(), binding.cbSelectInventory);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleSelection(Long id, android.widget.CheckBox checkBox) {
|
|
||||||
if (id == null)
|
|
||||||
return;
|
|
||||||
if (selectedIds.contains(id)) {
|
|
||||||
selectedIds.remove(id);
|
|
||||||
checkBox.setChecked(false);
|
|
||||||
} else {
|
|
||||||
selectedIds.add(id);
|
|
||||||
checkBox.setChecked(true);
|
|
||||||
}
|
|
||||||
clickListener.onSelectionChanged(selectedIds.size());
|
|
||||||
if (selectedIds.isEmpty()) {
|
|
||||||
selectionMode = false;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Long> getSelectedIds() {
|
|
||||||
return new ArrayList<>(selectedIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearSelection() {
|
|
||||||
selectedIds.clear();
|
|
||||||
selectionMode = false;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isInSelectionMode() {
|
|
||||||
return selectionMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
return inventoryList.size();
|
return inventoryList.size();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.example.petstoremobile.adapters;
|
|||||||
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
@@ -10,25 +11,41 @@ import com.example.petstoremobile.R;
|
|||||||
import com.example.petstoremobile.api.PetApi;
|
import com.example.petstoremobile.api.PetApi;
|
||||||
import com.example.petstoremobile.databinding.ItemPetBinding;
|
import com.example.petstoremobile.databinding.ItemPetBinding;
|
||||||
import com.example.petstoremobile.dtos.PetDTO;
|
import com.example.petstoremobile.dtos.PetDTO;
|
||||||
|
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||||
import com.example.petstoremobile.utils.GlideUtils;
|
import com.example.petstoremobile.utils.GlideUtils;
|
||||||
|
import com.example.petstoremobile.utils.SelectionHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
|
public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||||
|
|
||||||
private List<PetDTO> petList;
|
private List<PetDTO> petList;
|
||||||
private OnPetClickListener petClickListener;
|
private OnPetClickListener petClickListener;
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
private String token;
|
private String token;
|
||||||
|
private final SelectionHelper selectionHelper;
|
||||||
|
|
||||||
// Interface for pet click on recycler view
|
// Interface for pet click on recycler view
|
||||||
public interface OnPetClickListener {
|
public interface OnPetClickListener {
|
||||||
void onPetClick(int position);
|
void onPetClick(int position);
|
||||||
|
void onSelectionChanged(int selectedCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Constructor
|
//Constructor
|
||||||
public PetAdapter(List<PetDTO> petList, OnPetClickListener petClickListener) {
|
public PetAdapter(List<PetDTO> petList, OnPetClickListener petClickListener) {
|
||||||
this.petList = petList;
|
this.petList = petList;
|
||||||
this.petClickListener = petClickListener;
|
this.petClickListener = petClickListener;
|
||||||
|
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||||
|
@Override
|
||||||
|
public void onSelectionChanged(int count) {
|
||||||
|
petClickListener.onSelectionChanged(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSelectionModeToggle(boolean selectionMode) {
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBaseUrl(String baseUrl) {
|
public void setBaseUrl(String baseUrl) {
|
||||||
@@ -39,6 +56,16 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
|
|||||||
this.token = token;
|
this.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Long> getSelectedIds() {
|
||||||
|
return selectionHelper.getSelectedIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearSelection() {
|
||||||
|
selectionHelper.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
// Get the controls of each row in recycler view
|
// Get the controls of each row in recycler view
|
||||||
public static class PetViewHolder extends RecyclerView.ViewHolder {
|
public static class PetViewHolder extends RecyclerView.ViewHolder {
|
||||||
private final ItemPetBinding binding;
|
private final ItemPetBinding binding;
|
||||||
@@ -91,8 +118,31 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
|
|||||||
binding.ivPetProfile.setImageResource(R.drawable.placeholder);
|
binding.ivPetProfile.setImageResource(R.drawable.placeholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bulk delete selection mode
|
||||||
|
if (selectionHelper.isInSelectionMode()) {
|
||||||
|
binding.cbSelectPet.setVisibility(View.VISIBLE);
|
||||||
|
binding.cbSelectPet.setChecked(selectionHelper.isSelected(pet.getPetId()));
|
||||||
|
} else {
|
||||||
|
binding.cbSelectPet.setVisibility(View.GONE);
|
||||||
|
binding.cbSelectPet.setChecked(false);
|
||||||
|
}
|
||||||
|
|
||||||
//when a row is clicked, open the detail view
|
//when a row is clicked, open the detail view
|
||||||
holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position));
|
holder.itemView.setOnClickListener(v -> {
|
||||||
|
if (selectionHelper.isInSelectionMode()) {
|
||||||
|
selectionHelper.toggleSelection(pet.getPetId());
|
||||||
|
notifyItemChanged(position);
|
||||||
|
} else {
|
||||||
|
petClickListener.onPetClick(position);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.itemView.setOnLongClickListener(v -> {
|
||||||
|
if (!selectionHelper.isInSelectionMode()) {
|
||||||
|
selectionHelper.startSelection(pet.getPetId());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.petstoremobile.api;
|
package com.example.petstoremobile.api;
|
||||||
|
|
||||||
|
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||||
import com.example.petstoremobile.dtos.PageResponse;
|
import com.example.petstoremobile.dtos.PageResponse;
|
||||||
import com.example.petstoremobile.dtos.PetDTO;
|
import com.example.petstoremobile.dtos.PetDTO;
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ import retrofit2.Call;
|
|||||||
import retrofit2.http.Body;
|
import retrofit2.http.Body;
|
||||||
import retrofit2.http.DELETE;
|
import retrofit2.http.DELETE;
|
||||||
import retrofit2.http.GET;
|
import retrofit2.http.GET;
|
||||||
|
import retrofit2.http.HTTP;
|
||||||
import retrofit2.http.Multipart;
|
import retrofit2.http.Multipart;
|
||||||
import retrofit2.http.POST;
|
import retrofit2.http.POST;
|
||||||
import retrofit2.http.PUT;
|
import retrofit2.http.PUT;
|
||||||
@@ -48,6 +50,10 @@ public interface PetApi {
|
|||||||
@DELETE("api/v1/pets/{id}")
|
@DELETE("api/v1/pets/{id}")
|
||||||
Call<Void> deletePet(@Path("id") Long id);
|
Call<Void> deletePet(@Path("id") Long id);
|
||||||
|
|
||||||
|
// Bulk delete pets
|
||||||
|
@HTTP(method = "DELETE", path = "api/v1/pets", hasBody = true)
|
||||||
|
Call<Void> bulkDeletePets(@Body BulkDeleteRequest request);
|
||||||
|
|
||||||
// Upload pet image
|
// Upload pet image
|
||||||
@Multipart
|
@Multipart
|
||||||
@POST("api/v1/pets/{id}/image")
|
@POST("api/v1/pets/{id}/image")
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.example.petstoremobile.databinding.FragmentInventoryBinding;
|
|||||||
import com.example.petstoremobile.dtos.InventoryDTO;
|
import com.example.petstoremobile.dtos.InventoryDTO;
|
||||||
import com.example.petstoremobile.dtos.StoreDTO;
|
import com.example.petstoremobile.dtos.StoreDTO;
|
||||||
import com.example.petstoremobile.fragments.ListFragment;
|
import com.example.petstoremobile.fragments.ListFragment;
|
||||||
|
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||||
import com.example.petstoremobile.viewmodels.InventoryViewModel;
|
import com.example.petstoremobile.viewmodels.InventoryViewModel;
|
||||||
import com.example.petstoremobile.utils.Resource;
|
import com.example.petstoremobile.utils.Resource;
|
||||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||||
@@ -44,6 +45,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
|||||||
private List<StoreDTO> storeList = new ArrayList<>();
|
private List<StoreDTO> storeList = new ArrayList<>();
|
||||||
private InventoryAdapter adapter;
|
private InventoryAdapter adapter;
|
||||||
private InventoryViewModel viewModel;
|
private InventoryViewModel viewModel;
|
||||||
|
private BulkDeleteHandler bulkDeleteHandler;
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
private int currentPage = 0;
|
private int currentPage = 0;
|
||||||
@@ -72,6 +74,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
|||||||
setupStoreFilter();
|
setupStoreFilter();
|
||||||
setupSwipeRefresh();
|
setupSwipeRefresh();
|
||||||
setupFilterToggle();
|
setupFilterToggle();
|
||||||
|
setupBulkDelete();
|
||||||
loadInventory(true);
|
loadInventory(true);
|
||||||
loadStoreData();
|
loadStoreData();
|
||||||
|
|
||||||
@@ -87,11 +90,25 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
binding.btnBulkDelete.setOnClickListener(v -> confirmBulkDelete());
|
|
||||||
|
|
||||||
return binding.getRoot();
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupBulkDelete() {
|
||||||
|
bulkDeleteHandler = new BulkDeleteHandler(
|
||||||
|
this,
|
||||||
|
binding.layoutBulkDelete,
|
||||||
|
binding.tvSelectionCount,
|
||||||
|
binding.btnBulkDelete,
|
||||||
|
new BulkDeleteHandler.SelectableAdapter() {
|
||||||
|
@Override public List<Long> getSelectedIds() { return adapter.getSelectedIds(); }
|
||||||
|
@Override public void clearSelection() { adapter.clearSelection(); }
|
||||||
|
},
|
||||||
|
"inventory item",
|
||||||
|
viewModel::bulkDeleteInventory,
|
||||||
|
() -> loadInventory(true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
@@ -243,50 +260,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays a confirmation dialog before performing a bulk deletion of selected items.
|
|
||||||
*/
|
|
||||||
private void confirmBulkDelete() {
|
|
||||||
List<Long> ids = adapter.getSelectedIds();
|
|
||||||
if (ids.isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle("Delete " + ids.size() + " item(s)?")
|
|
||||||
.setMessage("This cannot be undone.")
|
|
||||||
.setPositiveButton("Delete", (d, w) -> bulkDelete(ids))
|
|
||||||
.setNegativeButton("Cancel", null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the bulk deletion of inventory items through the ViewModel.
|
|
||||||
*/
|
|
||||||
private void bulkDelete(List<Long> ids) {
|
|
||||||
viewModel.bulkDeleteInventory(ids).observe(getViewLifecycleOwner(), resource -> {
|
|
||||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
|
||||||
if (resource.status == Resource.Status.SUCCESS) {
|
|
||||||
adapter.clearSelection();
|
|
||||||
hideBulkDeleteBar();
|
|
||||||
loadInventory(true);
|
|
||||||
Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the bulk deletion UI bar.
|
|
||||||
*/
|
|
||||||
private void hideBulkDeleteBar() {
|
|
||||||
if (binding != null) {
|
|
||||||
binding.btnBulkDelete.setVisibility(View.GONE);
|
|
||||||
binding.tvSelectionCount.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to the inventory detail screen for a specific item or to add a new one.
|
* Navigates to the inventory detail screen for a specific item or to add a new one.
|
||||||
*/
|
*/
|
||||||
@@ -322,12 +295,8 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onSelectionChanged(int selectedCount) {
|
public void onSelectionChanged(int selectedCount) {
|
||||||
if (selectedCount > 0) {
|
if (bulkDeleteHandler != null) {
|
||||||
binding.btnBulkDelete.setVisibility(View.VISIBLE);
|
bulkDeleteHandler.onSelectionChanged(selectedCount);
|
||||||
binding.tvSelectionCount.setVisibility(View.VISIBLE);
|
|
||||||
binding.tvSelectionCount.setText(selectedCount + " selected");
|
|
||||||
} else {
|
|
||||||
hideBulkDeleteBar();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.example.petstoremobile.databinding.FragmentPetBinding;
|
|||||||
import com.example.petstoremobile.dtos.PetDTO;
|
import com.example.petstoremobile.dtos.PetDTO;
|
||||||
import com.example.petstoremobile.dtos.StoreDTO;
|
import com.example.petstoremobile.dtos.StoreDTO;
|
||||||
import com.example.petstoremobile.fragments.ListFragment;
|
import com.example.petstoremobile.fragments.ListFragment;
|
||||||
|
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||||
import com.example.petstoremobile.utils.Resource;
|
import com.example.petstoremobile.utils.Resource;
|
||||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||||
import com.example.petstoremobile.viewmodels.PetViewModel;
|
import com.example.petstoremobile.viewmodels.PetViewModel;
|
||||||
@@ -46,6 +47,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
private PetAdapter adapter;
|
private PetAdapter adapter;
|
||||||
private PetViewModel viewModel;
|
private PetViewModel viewModel;
|
||||||
private StoreViewModel storeViewModel;
|
private StoreViewModel storeViewModel;
|
||||||
|
private BulkDeleteHandler bulkDeleteHandler;
|
||||||
|
|
||||||
@Inject @Named("baseUrl") String baseUrl;
|
@Inject @Named("baseUrl") String baseUrl;
|
||||||
|
|
||||||
@@ -74,6 +76,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
setupStoreFilter();
|
setupStoreFilter();
|
||||||
setupSwipeRefresh();
|
setupSwipeRefresh();
|
||||||
setupFilterToggle();
|
setupFilterToggle();
|
||||||
|
setupBulkDelete();
|
||||||
|
|
||||||
binding.fabAddPet.setOnClickListener(v -> openPetDetails());
|
binding.fabAddPet.setOnClickListener(v -> openPetDetails());
|
||||||
|
|
||||||
@@ -90,6 +93,22 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
return binding.getRoot();
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupBulkDelete() {
|
||||||
|
bulkDeleteHandler = new BulkDeleteHandler(
|
||||||
|
this,
|
||||||
|
binding.layoutBulkDelete,
|
||||||
|
binding.tvSelectionCount,
|
||||||
|
binding.btnBulkDelete,
|
||||||
|
new BulkDeleteHandler.SelectableAdapter() {
|
||||||
|
@Override public List<Long> getSelectedIds() { return adapter.getSelectedIds(); }
|
||||||
|
@Override public void clearSelection() { adapter.clearSelection(); }
|
||||||
|
},
|
||||||
|
"pet",
|
||||||
|
viewModel::bulkDeletePets,
|
||||||
|
this::loadPetData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
@@ -231,6 +250,13 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
openPetProfile(position);
|
openPetProfile(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSelectionChanged(int selectedCount) {
|
||||||
|
if (bulkDeleteHandler != null) {
|
||||||
|
bulkDeleteHandler.onSelectionChanged(selectedCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches pet data from the server with all active filters.
|
* Fetches pet data from the server with all active filters.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.example.petstoremobile.repositories;
|
|||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
|
|
||||||
import com.example.petstoremobile.api.PetApi;
|
import com.example.petstoremobile.api.PetApi;
|
||||||
|
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||||
import com.example.petstoremobile.dtos.PageResponse;
|
import com.example.petstoremobile.dtos.PageResponse;
|
||||||
import com.example.petstoremobile.dtos.PetDTO;
|
import com.example.petstoremobile.dtos.PetDTO;
|
||||||
import com.example.petstoremobile.utils.Resource;
|
import com.example.petstoremobile.utils.Resource;
|
||||||
@@ -57,6 +58,13 @@ public class PetRepository extends BaseRepository {
|
|||||||
return executeCall(petApi.deletePet(id));
|
return executeCall(petApi.deletePet(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a request to the API to delete multiple pet records.
|
||||||
|
*/
|
||||||
|
public LiveData<Resource<Void>> bulkDeletePets(BulkDeleteRequest request) {
|
||||||
|
return executeCall(petApi.bulkDeletePets(request));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads an image file for a specific pet via the API.
|
* Uploads an image file for a specific pet via the API.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.example.petstoremobile.utils;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class to handle the UI and logic for bulk deletion across different fragments.
|
||||||
|
*/
|
||||||
|
public class BulkDeleteHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that adapters must implement to support bulk selection.
|
||||||
|
*/
|
||||||
|
public interface SelectableAdapter {
|
||||||
|
List<Long> getSelectedIds();
|
||||||
|
void clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional interface for the API call execution.
|
||||||
|
*/
|
||||||
|
public interface BulkDeleteOperation {
|
||||||
|
LiveData<Resource<Void>> execute(List<Long> ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Fragment fragment;
|
||||||
|
private final View layoutBar;
|
||||||
|
private final TextView tvCount;
|
||||||
|
private final SelectableAdapter adapter;
|
||||||
|
private final BulkDeleteOperation operation;
|
||||||
|
private final Runnable onSuccess;
|
||||||
|
private final String itemName;
|
||||||
|
|
||||||
|
public BulkDeleteHandler(Fragment fragment,
|
||||||
|
View layoutBar,
|
||||||
|
TextView tvCount,
|
||||||
|
Button btnDelete,
|
||||||
|
SelectableAdapter adapter,
|
||||||
|
String itemName,
|
||||||
|
BulkDeleteOperation operation,
|
||||||
|
Runnable onSuccess) {
|
||||||
|
this.fragment = fragment;
|
||||||
|
this.layoutBar = layoutBar;
|
||||||
|
this.tvCount = tvCount;
|
||||||
|
this.adapter = adapter;
|
||||||
|
this.operation = operation;
|
||||||
|
this.onSuccess = onSuccess;
|
||||||
|
this.itemName = itemName;
|
||||||
|
|
||||||
|
btnDelete.setOnClickListener(v -> confirmDelete());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the UI when the selection count changes.
|
||||||
|
*/
|
||||||
|
public void onSelectionChanged(int selectedCount) {
|
||||||
|
if (selectedCount > 0) {
|
||||||
|
layoutBar.setVisibility(View.VISIBLE);
|
||||||
|
tvCount.setText(selectedCount + " selected");
|
||||||
|
} else {
|
||||||
|
hideBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the bulk delete bar and resets state.
|
||||||
|
*/
|
||||||
|
public void hideBar() {
|
||||||
|
if (layoutBar != null) {
|
||||||
|
layoutBar.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the confirmation dialog.
|
||||||
|
*/
|
||||||
|
private void confirmDelete() {
|
||||||
|
List<Long> ids = adapter.getSelectedIds();
|
||||||
|
if (ids.isEmpty()) return;
|
||||||
|
|
||||||
|
DialogUtils.showBulkDeleteConfirmDialog(fragment.requireContext(), ids.size(), () -> performDelete(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the deletion via the provided operation.
|
||||||
|
*/
|
||||||
|
private void performDelete(List<Long> ids) {
|
||||||
|
operation.execute(ids).observe(fragment.getViewLifecycleOwner(), resource -> {
|
||||||
|
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||||
|
if (resource.status == Resource.Status.SUCCESS) {
|
||||||
|
adapter.clearSelection();
|
||||||
|
hideBar();
|
||||||
|
onSuccess.run();
|
||||||
|
Toast.makeText(fragment.getContext(), ids.size() + " " + itemName + "(s) deleted", Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(fragment.getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,18 @@ public class DialogUtils {
|
|||||||
showConfirmDialog(context, "Delete " + itemName + "?", "Are you sure you want to delete this " + itemName.toLowerCase() + "? This action cannot be undone.", callback);
|
showConfirmDialog(context, "Delete " + itemName + "?", "Are you sure you want to delete this " + itemName.toLowerCase() + "? This action cannot be undone.", callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a confirmation dialog with specific "Delete" and "Cancel" buttons.
|
||||||
|
*/
|
||||||
|
public static void showBulkDeleteConfirmDialog(Context context, int count, DialogCallback callback) {
|
||||||
|
new AlertDialog.Builder(context)
|
||||||
|
.setTitle("Delete " + count + " item(s)?")
|
||||||
|
.setMessage("This cannot be undone.")
|
||||||
|
.setPositiveButton("Delete", (dialog, which) -> callback.onConfirm())
|
||||||
|
.setNegativeButton("Cancel", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a simple information or error dialog with an "OK" button.
|
* Shows a simple information or error dialog with an "OK" button.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.example.petstoremobile.utils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to manage selection state in Adapters for bulk operations.
|
||||||
|
*/
|
||||||
|
public class SelectionHelper {
|
||||||
|
|
||||||
|
private final List<Long> selectedIds = new ArrayList<>();
|
||||||
|
private boolean selectionMode = false;
|
||||||
|
private final SelectionListener listener;
|
||||||
|
|
||||||
|
public interface SelectionListener {
|
||||||
|
void onSelectionChanged(int count);
|
||||||
|
void onSelectionModeToggle(boolean selectionMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SelectionHelper(SelectionListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleSelection(Long id) {
|
||||||
|
if (id == null) return;
|
||||||
|
|
||||||
|
if (selectedIds.contains(id)) {
|
||||||
|
selectedIds.remove(id);
|
||||||
|
} else {
|
||||||
|
selectedIds.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.onSelectionChanged(selectedIds.size());
|
||||||
|
|
||||||
|
if (selectedIds.isEmpty() && selectionMode) {
|
||||||
|
selectionMode = false;
|
||||||
|
listener.onSelectionModeToggle(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startSelection(Long id) {
|
||||||
|
selectionMode = true;
|
||||||
|
selectedIds.add(id);
|
||||||
|
listener.onSelectionChanged(selectedIds.size());
|
||||||
|
listener.onSelectionModeToggle(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSelected(Long id) {
|
||||||
|
return selectedIds.contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInSelectionMode() {
|
||||||
|
return selectionMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Long> getSelectedIds() {
|
||||||
|
return new ArrayList<>(selectedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearSelection() {
|
||||||
|
selectedIds.clear();
|
||||||
|
selectionMode = false;
|
||||||
|
listener.onSelectionChanged(0);
|
||||||
|
listener.onSelectionModeToggle(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,14 @@ package com.example.petstoremobile.viewmodels;
|
|||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.ViewModel;
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
|
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||||
import com.example.petstoremobile.dtos.PageResponse;
|
import com.example.petstoremobile.dtos.PageResponse;
|
||||||
import com.example.petstoremobile.dtos.PetDTO;
|
import com.example.petstoremobile.dtos.PetDTO;
|
||||||
import com.example.petstoremobile.repositories.PetRepository;
|
import com.example.petstoremobile.repositories.PetRepository;
|
||||||
import com.example.petstoremobile.utils.Resource;
|
import com.example.petstoremobile.utils.Resource;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel;
|
import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||||
@@ -57,6 +60,13 @@ public class PetViewModel extends ViewModel {
|
|||||||
return repository.deletePet(id);
|
return repository.deletePet(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple pet records.
|
||||||
|
*/
|
||||||
|
public LiveData<Resource<Void>> bulkDeletePets(List<Long> ids) {
|
||||||
|
return repository.bulkDeletePets(new BulkDeleteRequest(ids));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads an image for a specific pet.
|
* Uploads an image for a specific pet.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -132,6 +132,37 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutBulkDelete"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:background="@color/primary_medium"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSelectionCount"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="0 selected"
|
||||||
|
android:textColor="@color/white"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnBulkDelete"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Delete Selected"
|
||||||
|
android:backgroundTint="@color/accent_coral"
|
||||||
|
android:textColor="@color/white"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/swipeRefreshPet"
|
android:id="@+id/swipeRefreshPet"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -3,12 +3,27 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="horizontal"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingEnd="16dp"
|
||||||
android:paddingTop="16dp"
|
android:paddingTop="16dp"
|
||||||
android:paddingBottom="16dp"
|
android:paddingBottom="16dp"
|
||||||
android:background="@color/white">
|
android:background="@color/white"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/cbSelectPet"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -116,3 +131,5 @@
|
|||||||
android:layout_marginTop="12dp"/>
|
android:layout_marginTop="12dp"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
Reference in New Issue
Block a user