diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java index 300b7e57..a44ec993 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java @@ -1,10 +1,16 @@ package com.example.petstoremobile.adapters; import android.view.*; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.example.petstoremobile.R; +import com.example.petstoremobile.api.ProductApi; +import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.ProductDTO; import java.util.List; @@ -24,6 +30,7 @@ public class ProductAdapter extends RecyclerView.Adapter listener.onProductClick(position)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java index e2eb3090..ff7b79a7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -48,4 +48,8 @@ public interface PetApi { @POST("api/v1/pets/{id}/image") Call uploadPetImage(@Path("id") Long id, @Part MultipartBody.Part image); + // Delete pet image + @DELETE("api/v1/pets/{id}/image") + Call deletePetImage(@Path("id") Long id); + } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java index 422a39e5..dc02fd6c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java @@ -2,10 +2,12 @@ package com.example.petstoremobile.api; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; +import okhttp3.MultipartBody; import retrofit2.Call; import retrofit2.http.*; public interface ProductApi { + String PRODUCT_IMAGE_PATH = "api/v1/products/%d/image"; @GET("api/v1/products") Call> getAllProducts( @@ -24,4 +26,11 @@ public interface ProductApi { @DELETE("api/v1/products/{id}") Call deleteProduct(@Path("id") Long id); + + @Multipart + @POST("api/v1/products/{id}/image") + Call uploadProductImage(@Path("id") Long id, @Part MultipartBody.Part image); + + @DELETE("api/v1/products/{id}/image") + Call deleteProductImage(@Path("id") Long id); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java index 75605083..88c5312c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java @@ -8,6 +8,7 @@ import java.util.Map; import okhttp3.MultipartBody; import retrofit2.Call; import retrofit2.http.Body; +import retrofit2.http.DELETE; import retrofit2.http.GET; import retrofit2.http.Multipart; import retrofit2.http.POST; @@ -37,4 +38,8 @@ public interface AuthApi { @POST("api/v1/auth/me/avatar") Call uploadAvatar(@Part MultipartBody.Part avatar); + //delete avatar endpoint + @DELETE("api/v1/auth/me/avatar") + Call deleteAvatar(); + } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index c7253c70..b29c038d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -44,7 +44,9 @@ import com.google.gson.Gson; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import okhttp3.MediaType; @@ -61,6 +63,7 @@ public class ProfileFragment extends Fragment { private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole; private Uri photoUri; private UserDTO currentUser; + private boolean hasImage = false; //Initialize the launchers for camera and gallery private ActivityResultLauncher galleryLauncher; @@ -149,12 +152,20 @@ public class ProfileFragment extends Fragment { //Set up listeners for the buttons //Change photo button btnChangePhoto.setOnClickListener(v -> { + List options = new ArrayList<>(); + options.add("Take Photo"); + options.add("Choose from Gallery"); + if (hasImage) { + options.add("Remove Photo"); + } + //Show alert dialog to user to select from gallery or camera new AlertDialog.Builder(requireContext()) .setTitle("Change Profile Photo") //set the options for the alert dialog - .setItems(new String[]{"Take Photo", "Choose from Gallery"}, (dialog, which) -> { - if (which == 0) { + .setItems(options.toArray(new String[0]), (dialog, which) -> { + String selected = options.get(which); + if (selected.equals("Take Photo")) { // Choose Camera //Checks if the user has granted the camera permission already if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { @@ -164,11 +175,13 @@ public class ProfileFragment extends Fragment { //otherwise request the permission permissionLauncher.launch(Manifest.permission.CAMERA); } - } else { + } else if (selected.equals("Choose from Gallery")) { // Choose Gallery Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); galleryLauncher.launch(intent); + } else if (selected.equals("Remove Photo")) { + deleteAvatar(); } }) .show(); @@ -294,9 +307,23 @@ public class ProfileFragment extends Fragment { .skipMemoryCache(true) .placeholder(R.drawable.placeholder) .error(R.drawable.placeholder) + .listener(new com.bumptech.glide.request.RequestListener() { + @Override + public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target target, boolean isFirstResource) { + hasImage = false; + return false; + } + + @Override + public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) { + hasImage = true; + return false; + } + }) .into(imgProfile); } else { // load placeholder image if token is null + hasImage = false; Glide.with(ProfileFragment.this) .load(R.drawable.placeholder) .into(imgProfile); @@ -352,6 +379,28 @@ public class ProfileFragment extends Fragment { } } + private void deleteAvatar() { + AuthApi authApi = RetrofitClient.getAuthApi(requireContext()); + authApi.deleteAvatar().enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(requireContext(), "Avatar removed successfully", Toast.LENGTH_SHORT).show(); + hasImage = false; + imgProfile.setImageResource(R.drawable.placeholder); + } else { + Toast.makeText(requireContext(), "Failed to remove avatar", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("DELETE_AVATAR", "Failure: " + t.getMessage()); + Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + // Helper function to create a temporary File object from a Uri for uploading the avatar private File getFileFromUri(Uri uri) { try { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 4e72b6cd..e8b29611 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -36,6 +36,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc setupRecyclerView(view); setupSearch(view); setupSwipeRefresh(view); + loadProducts(); FloatingActionButton fab = view.findViewById(R.id.fabAddProduct); @@ -63,7 +64,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc public void beforeTextChanged(CharSequence s, int a, int b, int c) {} public void afterTextChanged(Editable s) {} public void onTextChanged(CharSequence s, int a, int b, int c) { - filter(s.toString()); + filter(); } }); } @@ -73,17 +74,18 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc swipeRefresh.setOnRefreshListener(this::loadProducts); } - private void filter(String query) { + private void filter() { + String query = etSearch.getText().toString().toLowerCase(); + filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(productList); - } else { - String lower = query.toLowerCase(); - for (ProductDTO p : productList) { - if ((p.getProdName() != null && p.getProdName().toLowerCase().contains(lower)) - || (p.getCategoryName() != null && p.getCategoryName().toLowerCase().contains(lower))) { - filteredList.add(p); - } + for (ProductDTO p : productList) { + boolean matchesSearch = query.isEmpty() || + (p.getProdName() != null && p.getProdName().toLowerCase().contains(query)) || + (p.getCategoryName() != null && p.getCategoryName().toLowerCase().contains(query)) || + (p.getProdDesc() != null && p.getProdDesc().toLowerCase().contains(query)); + + if (matchesSearch) { + filteredList.add(p); } } adapter.notifyDataSetChanged(); @@ -99,7 +101,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc if (r.isSuccessful() && r.body() != null) { productList.clear(); productList.addAll(r.body().getContent()); - filter(etSearch != null ? etSearch.getText().toString() : ""); + filter(); } else { Toast.makeText(getContext(), "Failed to load products", Toast.LENGTH_SHORT).show(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 8427ab65..40bdc91b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -1,19 +1,37 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; +import android.provider.MediaStore; import android.util.Log; import android.view.*; import android.widget.*; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.fragments.ListFragment; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; import java.math.BigDecimal; import java.util.*; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; import retrofit2.*; public class ProductDetailFragment extends Fragment { @@ -22,12 +40,59 @@ public class ProductDetailFragment extends Fragment { private EditText etProductName, etProductDesc, etProductPrice; private Spinner spinnerCategory; private Button btnSave, btnDelete, btnBack; + private ImageView ivProductImage; private long prodId = -1; private boolean isEditing = false; private long preselectedCategoryId = -1; + private boolean hasImage = false; private List categoryList = new ArrayList<>(); + private Uri photoUri; + + private ActivityResultLauncher galleryLauncher; + private ActivityResultLauncher cameraLauncher; + private ActivityResultLauncher permissionLauncher; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + galleryLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + Uri selectedImage = result.getData().getData(); + if (isEditing) { + uploadProductImage(selectedImage); + } else { + ivProductImage.setImageURI(selectedImage); + photoUri = selectedImage; + hasImage = true; + } + } + } + ); + cameraLauncher = registerForActivityResult( + new ActivityResultContracts.TakePicture(), + success -> { + if (success) { + if (isEditing) { + uploadProductImage(photoUri); + } else { + ivProductImage.setImageURI(photoUri); + hasImage = true; + } + } + } + ); + permissionLauncher = registerForActivityResult( + new ActivityResultContracts.RequestPermission(), + granted -> { + if (granted) launchCamera(); + else Toast.makeText(getContext(), "Camera permission denied", Toast.LENGTH_SHORT).show(); + } + ); + } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -40,6 +105,7 @@ public class ProductDetailFragment extends Fragment { btnBack.setOnClickListener(v -> navigateBack()); btnSave.setOnClickListener(v -> saveProduct()); btnDelete.setOnClickListener(v -> confirmDelete()); + ivProductImage.setOnClickListener(v -> showImagePickerDialog()); return view; } @@ -53,6 +119,71 @@ public class ProductDetailFragment extends Fragment { btnSave = v.findViewById(R.id.btnSaveProduct); btnDelete = v.findViewById(R.id.btnDeleteProduct); btnBack = v.findViewById(R.id.btnProductBack); + ivProductImage = v.findViewById(R.id.ivProductImage); + } + + // Helper function to show the image picker dialog + private void showImagePickerDialog() { + List options = new ArrayList<>(); + options.add("Take Photo"); + options.add("Choose from Gallery"); + if (hasImage) { + options.add("Remove Photo"); + } + + new AlertDialog.Builder(requireContext()) + .setTitle("Select Product Image") + .setItems(options.toArray(new String[0]), (dialog, which) -> { + String selectedOption = options.get(which); + if (selectedOption.equals("Take Photo")) { + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED) { + launchCamera(); + } else { + permissionLauncher.launch(Manifest.permission.CAMERA); + } + } else if (selectedOption.equals("Choose from Gallery")) { + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + galleryLauncher.launch(intent); + } else if (selectedOption.equals("Remove Photo")) { + removePhoto(); + } + }) + .show(); + } + + // Helper function to remove the photo + private void removePhoto() { + if (isEditing) { + RetrofitClient.getProductApi(requireContext()).deleteProductImage(prodId) + .enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Photo removed", Toast.LENGTH_SHORT).show(); + ivProductImage.setImageResource(R.drawable.placeholder2); + hasImage = false; + } else { + Toast.makeText(getContext(), "Failed to remove photo", Toast.LENGTH_SHORT).show(); + } + } + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } else { + photoUri = null; + hasImage = false; + ivProductImage.setImageResource(R.drawable.placeholder2); + } + } + + // Helper function to launch the camera + private void launchCamera() { + File photoFile = new File(requireContext().getCacheDir(), "product_photo.jpg"); + photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile); + cameraLauncher.launch(photoUri); } private void loadCategories() { @@ -92,6 +223,7 @@ public class ProductDetailFragment extends Fragment { isEditing = true; prodId = a.getLong("prodId"); preselectedCategoryId = a.getLong("categoryId", -1); + hasImage = true; tvMode.setText("Edit Product"); tvProductId.setText("ID: " + prodId); @@ -100,10 +232,74 @@ public class ProductDetailFragment extends Fragment { etProductDesc.setText(a.getString("prodDesc")); etProductPrice.setText(a.getString("prodPrice")); btnDelete.setVisibility(View.VISIBLE); + loadProductImage(); } else { tvMode.setText("Add Product"); btnDelete.setVisibility(View.GONE); tvProductId.setVisibility(View.GONE); + hasImage = false; + } + } + + //load the product image from the backend + private void loadProductImage() { + String imageUrl = RetrofitClient.BASE_URL + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId); + Glide.with(this) + .load(imageUrl) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .placeholder(R.drawable.placeholder2) + .error(R.drawable.placeholder2) + .into(ivProductImage); + } + + // Function to upload the product image by calling the backend + private void uploadProductImage(Uri uri) { + try { + File file = getFileFromUri(uri); + if (file == null) return; + + RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); + MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); + + RetrofitClient.getProductApi(requireContext()).uploadProductImage(prodId, body) + .enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Image uploaded", Toast.LENGTH_SHORT).show(); + hasImage = true; + loadProductImage(); + } else { + Toast.makeText(getContext(), "Upload failed", Toast.LENGTH_SHORT).show(); + } + } + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } catch (Exception e) { + Log.e("ProductDetail", "Error uploading image", e); + } + } + + // Helper function to get the File from the Uri + private File getFileFromUri(Uri uri) { + try { + InputStream inputStream = requireContext().getContentResolver().openInputStream(uri); + File tempFile = new File(requireContext().getCacheDir(), "upload_product_image.jpg"); + FileOutputStream outputStream = new FileOutputStream(tempFile); + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, length); + } + outputStream.close(); + inputStream.close(); + return tempFile; + } catch (Exception e) { + return null; } } @@ -132,14 +328,30 @@ public class ProductDetailFragment extends Fragment { ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); - Log.d("PRODUCT_SAVE", "name=" + name + " categoryId=" + category.getCategoryId() - + " price=" + price); - ProductApi api = RetrofitClient.getProductApi(requireContext()); if (isEditing) { api.updateProduct(prodId, dto).enqueue(simpleCallback("Updated")); } else { - api.createProduct(dto).enqueue(simpleCallback("Saved")); + api.createProduct(dto).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + long newId = response.body().getProdId(); + if (photoUri != null) { + prodId = newId; + uploadProductImage(photoUri); + } + Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + Toast.makeText(getContext(), "Error saving", Toast.LENGTH_SHORT).show(); + } + } + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); } } @@ -150,17 +362,10 @@ public class ProductDetailFragment extends Fragment { Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); navigateBack(); } else { - try { - String err = r.errorBody().string(); - Log.e("PRODUCT_SAVE", "Error: " + err); - Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show(); - } catch (Exception e) { - Log.e("PRODUCT_SAVE", "Failed to read error"); - } + Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show(); } } public void onFailure(Call c, Throwable t) { - Log.e("PRODUCT_SAVE", "Failure: " + t.getMessage()); Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); } }; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 454d263b..c4c47d31 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -36,6 +36,8 @@ import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDet import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import okhttp3.MediaType; @@ -52,6 +54,7 @@ public class PetProfileFragment extends Fragment { private ImageView imgPet; private Uri photoUri; private int petId; + private boolean hasImage = false; // launchers for camera and gallery private ActivityResultLauncher galleryLauncher; @@ -162,10 +165,18 @@ public class PetProfileFragment extends Fragment { //Make change photo button ask user to select a new photo btnChangePhoto.setOnClickListener(v -> { + List options = new ArrayList<>(); + options.add("Take Photo"); + options.add("Choose from Gallery"); + if (hasImage) { + options.add("Remove Photo"); + } + new AlertDialog.Builder(requireContext()) .setTitle("Change Pet Photo") - .setItems(new String[]{"Take Photo", "Choose from Gallery"}, (dialog, which) -> { - if (which == 0) { + .setItems(options.toArray(new String[0]), (dialog, which) -> { + String selected = options.get(which); + if (selected.equals("Take Photo")) { // Choose Camera //Checks if the user has granted the camera permission already if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { @@ -175,9 +186,11 @@ public class PetProfileFragment extends Fragment { //otherwise request the permission permissionLauncher.launch(Manifest.permission.CAMERA); } - } else { + } else if (selected.equals("Choose from Gallery")) { Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); galleryLauncher.launch(intent); + } else if (selected.equals("Remove Photo")) { + deletePetImage(); } }) .show(); @@ -196,6 +209,19 @@ public class PetProfileFragment extends Fragment { .skipMemoryCache(true) .placeholder(R.drawable.placeholder) .error(R.drawable.placeholder) + .listener(new com.bumptech.glide.request.RequestListener() { + @Override + public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target target, boolean isFirstResource) { + hasImage = false; + return false; + } + + @Override + public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) { + hasImage = true; + return false; + } + }) .into(imgPet); } @@ -234,6 +260,28 @@ public class PetProfileFragment extends Fragment { } } + private void deletePetImage() { + PetApi petApi = RetrofitClient.getPetApi(requireContext()); + petApi.deletePetImage((long) petId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(requireContext(), "Pet photo removed", Toast.LENGTH_SHORT).show(); + hasImage = false; + imgPet.setImageResource(R.drawable.placeholder); + } else { + Toast.makeText(requireContext(), "Failed to remove pet photo", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("DELETE_PET_IMAGE", "Failure: " + t.getMessage()); + Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + // Helper function to create a temporary File object from a Uri for uploading private File getFileFromUri(Uri uri) { try { diff --git a/android/app/src/main/res/drawable/placeholder2.png b/android/app/src/main/res/drawable/placeholder2.png new file mode 100644 index 00000000..dec35ec5 Binary files /dev/null and b/android/app/src/main/res/drawable/placeholder2.png differ diff --git a/android/app/src/main/res/layout/fragment_product_detail.xml b/android/app/src/main/res/layout/fragment_product_detail.xml index b7d14a9d..7ab22faa 100644 --- a/android/app/src/main/res/layout/fragment_product_detail.xml +++ b/android/app/src/main/res/layout/fragment_product_detail.xml @@ -64,6 +64,32 @@ android:layout_gravity="end" android:layout_marginBottom="8dp"/> + + + + + + + + + - - - - - - + android:gravity="center_vertical"> - + + + android:orientation="vertical"> + + + + + + + + + + + +