Attachments to chat #133
@@ -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<ProductAdapter.ProductV
|
||||
|
||||
public static class ProductViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvName, tvCategory, tvDesc, tvPrice;
|
||||
ImageView ivProductImage;
|
||||
|
||||
public ProductViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
@@ -31,6 +38,7 @@ public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductV
|
||||
tvCategory = v.findViewById(R.id.tvProductCategory);
|
||||
tvDesc = v.findViewById(R.id.tvProductDesc);
|
||||
tvPrice = v.findViewById(R.id.tvProductPrice);
|
||||
ivProductImage = v.findViewById(R.id.ivProductImage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +57,18 @@ public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductV
|
||||
holder.tvCategory.setText("Category: " + (p.getCategoryName() != null ? p.getCategoryName() : ""));
|
||||
holder.tvDesc.setText(p.getProdDesc() != null ? p.getProdDesc() : "");
|
||||
holder.tvPrice.setText(p.getProdPrice() != null ? "$" + p.getProdPrice() : "");
|
||||
|
||||
// Load product image using Glide
|
||||
String imageUrl = RetrofitClient.BASE_URL + String.format(ProductApi.PRODUCT_IMAGE_PATH, p.getProdId());
|
||||
Glide.with(holder.itemView.getContext())
|
||||
.load(imageUrl)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.placeholder(R.drawable.placeholder)
|
||||
.error(R.drawable.placeholder)
|
||||
.into(holder.ivProductImage);
|
||||
|
||||
holder.itemView.setOnClickListener(v -> listener.onProductClick(position));
|
||||
}
|
||||
|
||||
|
||||
@@ -48,4 +48,8 @@ public interface PetApi {
|
||||
@POST("api/v1/pets/{id}/image")
|
||||
Call<Void> uploadPetImage(@Path("id") Long id, @Part MultipartBody.Part image);
|
||||
|
||||
// Delete pet image
|
||||
@DELETE("api/v1/pets/{id}/image")
|
||||
Call<Void> deletePetImage(@Path("id") Long id);
|
||||
|
||||
}
|
||||
|
||||
@@ -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<PageResponse<ProductDTO>> getAllProducts(
|
||||
@@ -24,4 +26,11 @@ public interface ProductApi {
|
||||
|
||||
@DELETE("api/v1/products/{id}")
|
||||
Call<Void> deleteProduct(@Path("id") Long id);
|
||||
|
||||
@Multipart
|
||||
@POST("api/v1/products/{id}/image")
|
||||
Call<Void> uploadProductImage(@Path("id") Long id, @Part MultipartBody.Part image);
|
||||
|
||||
@DELETE("api/v1/products/{id}/image")
|
||||
Call<Void> deleteProductImage(@Path("id") Long id);
|
||||
}
|
||||
@@ -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<UserDTO> uploadAvatar(@Part MultipartBody.Part avatar);
|
||||
|
||||
//delete avatar endpoint
|
||||
@DELETE("api/v1/auth/me/avatar")
|
||||
Call<Void> deleteAvatar();
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Intent> galleryLauncher;
|
||||
@@ -149,12 +152,20 @@ public class ProfileFragment extends Fragment {
|
||||
//Set up listeners for the buttons
|
||||
//Change photo button
|
||||
btnChangePhoto.setOnClickListener(v -> {
|
||||
List<String> 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<android.graphics.drawable.Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, boolean isFirstResource) {
|
||||
hasImage = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> 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<Void>() {
|
||||
@Override
|
||||
public void onResponse(Call<Void> call, Response<Void> 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<Void> 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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<CategoryDTO> categoryList = new ArrayList<>();
|
||||
private Uri photoUri;
|
||||
|
||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||
private ActivityResultLauncher<Uri> cameraLauncher;
|
||||
private ActivityResultLauncher<String> 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<String> 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<Void>() {
|
||||
@Override
|
||||
public void onResponse(Call<Void> call, Response<Void> 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<Void> 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<Void>() {
|
||||
@Override
|
||||
public void onResponse(Call<Void> call, Response<Void> 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<Void> 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<ProductDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<ProductDTO> call, Response<ProductDTO> 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<ProductDTO> 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<ProductDTO> c, Throwable t) {
|
||||
Log.e("PRODUCT_SAVE", "Failure: " + t.getMessage());
|
||||
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<Intent> 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<String> 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<android.graphics.drawable.Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target, boolean isFirstResource) {
|
||||
hasImage = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> 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<Void>() {
|
||||
@Override
|
||||
public void onResponse(Call<Void> call, Response<Void> 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<Void> 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 {
|
||||
|
||||
BIN
android/app/src/main/res/drawable/placeholder2.png
Normal file
BIN
android/app/src/main/res/drawable/placeholder2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
@@ -64,6 +64,32 @@
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<!-- Product Image -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Product Image"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivProductImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/placeholder2"
|
||||
android:background="@color/text_light"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Product Name -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
@@ -9,56 +10,83 @@
|
||||
android:paddingBottom="16dp"
|
||||
android:background="@color/white">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Product Name"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductCategory"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Category"
|
||||
android:textColor="#888888"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:text="Product Description"
|
||||
android:textColor="#888888"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="8dp">
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductPrice"
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/ivProductImage"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/placeholder"
|
||||
app:shapeAppearanceOverlay="@style/CircleImageView"
|
||||
app:strokeWidth="2dp"
|
||||
app:strokeColor="#BDBDBD"
|
||||
android:padding="2dp"
|
||||
android:contentDescription="Product Image" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="$0.00"
|
||||
android:textColor="@color/accent_coral"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Product Name"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductCategory"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="Category"
|
||||
android:textColor="#888888"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:text="Product Description"
|
||||
android:textColor="#888888"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductPrice"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="$0.00"
|
||||
android:textColor="@color/accent_coral"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
|
||||
Reference in New Issue
Block a user