Merge pull request #133 from RecentRunner/AttachmentsToChat
Attachments to chat
This commit is contained in:
@@ -82,6 +82,8 @@ dependencies {
|
|||||||
implementation("com.github.bumptech.glide:glide:4.16.0")
|
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||||
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
|
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
|
||||||
|
|
||||||
|
implementation("com.github.prolificinteractive:material-calendarview:2.0.1")
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.ext.junit)
|
androidTestImplementation(libs.ext.junit)
|
||||||
androidTestImplementation(libs.espresso.core)
|
androidTestImplementation(libs.espresso.core)
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package com.example.petstoremobile.adapters;
|
package com.example.petstoremobile.adapters;
|
||||||
|
|
||||||
import android.view.*;
|
import android.view.*;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
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.R;
|
||||||
|
import com.example.petstoremobile.api.ProductApi;
|
||||||
|
import com.example.petstoremobile.api.RetrofitClient;
|
||||||
import com.example.petstoremobile.dtos.ProductDTO;
|
import com.example.petstoremobile.dtos.ProductDTO;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -24,6 +30,7 @@ public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductV
|
|||||||
|
|
||||||
public static class ProductViewHolder extends RecyclerView.ViewHolder {
|
public static class ProductViewHolder extends RecyclerView.ViewHolder {
|
||||||
TextView tvName, tvCategory, tvDesc, tvPrice;
|
TextView tvName, tvCategory, tvDesc, tvPrice;
|
||||||
|
ImageView ivProductImage;
|
||||||
|
|
||||||
public ProductViewHolder(@NonNull View v) {
|
public ProductViewHolder(@NonNull View v) {
|
||||||
super(v);
|
super(v);
|
||||||
@@ -31,6 +38,7 @@ public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductV
|
|||||||
tvCategory = v.findViewById(R.id.tvProductCategory);
|
tvCategory = v.findViewById(R.id.tvProductCategory);
|
||||||
tvDesc = v.findViewById(R.id.tvProductDesc);
|
tvDesc = v.findViewById(R.id.tvProductDesc);
|
||||||
tvPrice = v.findViewById(R.id.tvProductPrice);
|
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.tvCategory.setText("Category: " + (p.getCategoryName() != null ? p.getCategoryName() : ""));
|
||||||
holder.tvDesc.setText(p.getProdDesc() != null ? p.getProdDesc() : "");
|
holder.tvDesc.setText(p.getProdDesc() != null ? p.getProdDesc() : "");
|
||||||
holder.tvPrice.setText(p.getProdPrice() != null ? "$" + p.getProdPrice() : "");
|
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));
|
holder.itemView.setOnClickListener(v -> listener.onProductClick(position));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,4 +48,8 @@ public interface PetApi {
|
|||||||
@POST("api/v1/pets/{id}/image")
|
@POST("api/v1/pets/{id}/image")
|
||||||
Call<Void> uploadPetImage(@Path("id") Long id, @Part MultipartBody.Part 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.PageResponse;
|
||||||
import com.example.petstoremobile.dtos.ProductDTO;
|
import com.example.petstoremobile.dtos.ProductDTO;
|
||||||
|
import okhttp3.MultipartBody;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.http.*;
|
import retrofit2.http.*;
|
||||||
|
|
||||||
public interface ProductApi {
|
public interface ProductApi {
|
||||||
|
String PRODUCT_IMAGE_PATH = "api/v1/products/%d/image";
|
||||||
|
|
||||||
@GET("api/v1/products")
|
@GET("api/v1/products")
|
||||||
Call<PageResponse<ProductDTO>> getAllProducts(
|
Call<PageResponse<ProductDTO>> getAllProducts(
|
||||||
@@ -24,4 +26,11 @@ public interface ProductApi {
|
|||||||
|
|
||||||
@DELETE("api/v1/products/{id}")
|
@DELETE("api/v1/products/{id}")
|
||||||
Call<Void> deleteProduct(@Path("id") Long 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 okhttp3.MultipartBody;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.http.Body;
|
import retrofit2.http.Body;
|
||||||
|
import retrofit2.http.DELETE;
|
||||||
import retrofit2.http.GET;
|
import retrofit2.http.GET;
|
||||||
import retrofit2.http.Multipart;
|
import retrofit2.http.Multipart;
|
||||||
import retrofit2.http.POST;
|
import retrofit2.http.POST;
|
||||||
@@ -37,4 +38,8 @@ public interface AuthApi {
|
|||||||
@POST("api/v1/auth/me/avatar")
|
@POST("api/v1/auth/me/avatar")
|
||||||
Call<UserDTO> uploadAvatar(@Part MultipartBody.Part avatar);
|
Call<UserDTO> uploadAvatar(@Part MultipartBody.Part avatar);
|
||||||
|
|
||||||
|
//delete avatar endpoint
|
||||||
|
@DELETE("api/v1/auth/me/avatar")
|
||||||
|
Call<Void> deleteAvatar();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ public class MessageDTO {
|
|||||||
@SerializedName("isRead")
|
@SerializedName("isRead")
|
||||||
private Boolean isRead;
|
private Boolean isRead;
|
||||||
|
|
||||||
|
@SerializedName("attachmentUrl")
|
||||||
|
private String attachmentUrl;
|
||||||
|
|
||||||
|
@SerializedName("attachmentName")
|
||||||
|
private String attachmentName;
|
||||||
|
|
||||||
|
@SerializedName("attachmentType")
|
||||||
|
private String attachmentType;
|
||||||
|
|
||||||
public MessageDTO() {}
|
public MessageDTO() {}
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
@@ -41,4 +50,13 @@ public class MessageDTO {
|
|||||||
|
|
||||||
public Boolean getIsRead() { return isRead; }
|
public Boolean getIsRead() { return isRead; }
|
||||||
public void setIsRead(Boolean isRead) { this.isRead = isRead; }
|
public void setIsRead(Boolean isRead) { this.isRead = isRead; }
|
||||||
|
|
||||||
|
public String getAttachmentUrl() { return attachmentUrl; }
|
||||||
|
public void setAttachmentUrl(String attachmentUrl) { this.attachmentUrl = attachmentUrl; }
|
||||||
|
|
||||||
|
public String getAttachmentName() { return attachmentName; }
|
||||||
|
public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; }
|
||||||
|
|
||||||
|
public String getAttachmentType() { return attachmentType; }
|
||||||
|
public void setAttachmentType(String attachmentType) { this.attachmentType = attachmentType; }
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,23 @@
|
|||||||
package com.example.petstoremobile.fragments;
|
package com.example.petstoremobile.fragments;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.*;
|
import android.view.*;
|
||||||
import android.widget.*;
|
import android.widget.*;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.core.view.GravityCompat;
|
import androidx.core.view.GravityCompat;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.adapters.ChatAdapter;
|
import com.example.petstoremobile.adapters.ChatAdapter;
|
||||||
import com.example.petstoremobile.adapters.MessageAdapter;
|
import com.example.petstoremobile.adapters.MessageAdapter;
|
||||||
@@ -41,8 +49,15 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
private RecyclerView rvChatList, rvMessages;
|
private RecyclerView rvChatList, rvMessages;
|
||||||
private EditText etMessage;
|
private EditText etMessage;
|
||||||
private Button btnSend;
|
private Button btnSend;
|
||||||
|
private ImageButton btnAttach;
|
||||||
private TextView tvChatTitle;
|
private TextView tvChatTitle;
|
||||||
|
|
||||||
|
// Preview views
|
||||||
|
private View layoutAttachmentPreview;
|
||||||
|
private ImageView ivPreview;
|
||||||
|
private TextView tvPreviewName;
|
||||||
|
private ImageButton btnRemoveAttachment;
|
||||||
|
|
||||||
// Adapters
|
// Adapters
|
||||||
private ChatAdapter chatAdapter;
|
private ChatAdapter chatAdapter;
|
||||||
private MessageAdapter messageAdapter;
|
private MessageAdapter messageAdapter;
|
||||||
@@ -51,6 +66,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
private final List<Chat> chatList = new ArrayList<>();
|
private final List<Chat> chatList = new ArrayList<>();
|
||||||
private final List<Message> messageList = new ArrayList<>();
|
private final List<Message> messageList = new ArrayList<>();
|
||||||
private final Map<Long, String> customerNames = new HashMap<>();
|
private final Map<Long, String> customerNames = new HashMap<>();
|
||||||
|
private Uri pendingAttachmentUri;
|
||||||
|
|
||||||
// APIs
|
// APIs
|
||||||
private ChatApi chatApi;
|
private ChatApi chatApi;
|
||||||
@@ -61,6 +77,24 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
private Long currentUserId;
|
private Long currentUserId;
|
||||||
private Long activeConversationId;
|
private Long activeConversationId;
|
||||||
private StompChatManager stompChatManager;
|
private StompChatManager stompChatManager;
|
||||||
|
private ActivityResultLauncher<Intent> attachmentLauncher;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
attachmentLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
Uri uri = result.getData().getData();
|
||||||
|
if (uri != null) {
|
||||||
|
showAttachmentPreview(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
@@ -77,11 +111,28 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
rvMessages = view.findViewById(R.id.rvMessages);
|
rvMessages = view.findViewById(R.id.rvMessages);
|
||||||
etMessage = view.findViewById(R.id.etMessage);
|
etMessage = view.findViewById(R.id.etMessage);
|
||||||
btnSend = view.findViewById(R.id.btnSend);
|
btnSend = view.findViewById(R.id.btnSend);
|
||||||
|
btnAttach = view.findViewById(R.id.btnAttach);
|
||||||
tvChatTitle = view.findViewById(R.id.tvChatTitle);
|
tvChatTitle = view.findViewById(R.id.tvChatTitle);
|
||||||
|
|
||||||
|
layoutAttachmentPreview = view.findViewById(R.id.layoutAttachmentPreview);
|
||||||
|
ivPreview = view.findViewById(R.id.ivPreview);
|
||||||
|
tvPreviewName = view.findViewById(R.id.tvPreviewName);
|
||||||
|
btnRemoveAttachment = view.findViewById(R.id.btnRemoveAttachment);
|
||||||
|
|
||||||
ImageButton hamburger = view.findViewById(R.id.btnHamburger);
|
ImageButton hamburger = view.findViewById(R.id.btnHamburger);
|
||||||
hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START));
|
hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START));
|
||||||
btnSend.setOnClickListener(v -> sendMessage());
|
//When the send button is clicked check if there is an attachment and send using the correct helper function
|
||||||
|
btnSend.setOnClickListener(v -> {
|
||||||
|
if (pendingAttachmentUri != null) {
|
||||||
|
sendWithAttachment(pendingAttachmentUri);
|
||||||
|
} else {
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//When the attachment button is clicked open the file picker
|
||||||
|
btnAttach.setOnClickListener(v -> selectAttachment());
|
||||||
|
btnRemoveAttachment.setOnClickListener(v -> removeAttachment());
|
||||||
|
|
||||||
setupRecyclerViews();
|
setupRecyclerViews();
|
||||||
loadInitialData();
|
loadInitialData();
|
||||||
@@ -89,6 +140,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to setup recycler views for chat and messages
|
||||||
private void setupRecyclerViews() {
|
private void setupRecyclerViews() {
|
||||||
// Set up Drawer menu to select conversation
|
// Set up Drawer menu to select conversation
|
||||||
chatAdapter = new ChatAdapter(chatList, this);
|
chatAdapter = new ChatAdapter(chatList, this);
|
||||||
@@ -273,6 +325,68 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Helper function to open file picker when the attachment button is clicked
|
||||||
|
private void selectAttachment() {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
|
intent.setType("*/*");
|
||||||
|
attachmentLauncher.launch(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Helper function to show the attachment preview
|
||||||
|
private void showAttachmentPreview(Uri uri) {
|
||||||
|
pendingAttachmentUri = uri;
|
||||||
|
layoutAttachmentPreview.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
String mimeType = requireContext().getContentResolver().getType(uri);
|
||||||
|
String fileName = getFileName(uri);
|
||||||
|
tvPreviewName.setText(fileName);
|
||||||
|
|
||||||
|
// If the file is an image, display a thumbnail of the image as well
|
||||||
|
if (mimeType != null && mimeType.startsWith("image/")) {
|
||||||
|
ivPreview.setVisibility(View.VISIBLE);
|
||||||
|
Glide.with(this).load(uri).into(ivPreview);
|
||||||
|
} else {
|
||||||
|
ivPreview.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Helper function to remove the attachment
|
||||||
|
private void removeAttachment() {
|
||||||
|
pendingAttachmentUri = null;
|
||||||
|
layoutAttachmentPreview.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Helper function to get the file name from the uri to display in attachment preview
|
||||||
|
private String getFileName(Uri uri) {
|
||||||
|
String result = null;
|
||||||
|
if (uri.getScheme().equals("content")) {
|
||||||
|
try (Cursor cursor = requireContext().getContentResolver().query(uri, null, null, null, null)) {
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||||
|
if (index != -1) {
|
||||||
|
result = cursor.getString(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result == null) {
|
||||||
|
result = uri.getPath();
|
||||||
|
int cut = result.lastIndexOf('/');
|
||||||
|
if (cut != -1) {
|
||||||
|
result = result.substring(cut + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Helper function to send the message with attachment
|
||||||
|
private void sendWithAttachment(Uri uri) {
|
||||||
|
if (activeConversationId == null) return;
|
||||||
|
|
||||||
|
//TODO: send the message with attachment when backend is done
|
||||||
|
Log.d(TAG, "Send with attachment happening");
|
||||||
|
}
|
||||||
|
|
||||||
// When a message is received updates the chat preview
|
// When a message is received updates the chat preview
|
||||||
@Override
|
@Override
|
||||||
public void onMessageReceived(MessageDTO dto) {
|
public void onMessageReceived(MessageDTO dto) {
|
||||||
@@ -370,6 +484,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
m.setContent(dto.getContent());
|
m.setContent(dto.getContent());
|
||||||
m.setTimestamp(dto.getTimestamp());
|
m.setTimestamp(dto.getTimestamp());
|
||||||
m.setIsRead(dto.getIsRead());
|
m.setIsRead(dto.getIsRead());
|
||||||
|
m.setAttachmentUrl(dto.getAttachmentUrl());
|
||||||
|
m.setAttachmentName(dto.getAttachmentName());
|
||||||
|
m.setAttachmentType(dto.getAttachmentType());
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,9 +524,11 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
private void setConversationActive(boolean active) {
|
private void setConversationActive(boolean active) {
|
||||||
btnSend.setEnabled(active);
|
btnSend.setEnabled(active);
|
||||||
etMessage.setEnabled(active);
|
etMessage.setEnabled(active);
|
||||||
|
btnAttach.setEnabled(active);
|
||||||
if (!active) {
|
if (!active) {
|
||||||
activeConversationId = null;
|
activeConversationId = null;
|
||||||
ChatNotificationService.activeConversationIdInUi = null;
|
ChatNotificationService.activeConversationIdInUi = null;
|
||||||
|
removeAttachment();
|
||||||
if (tvChatTitle != null) tvChatTitle.setText("Customer Chat");
|
if (tvChatTitle != null) tvChatTitle.setText("Customer Chat");
|
||||||
if (stompChatManager != null) {
|
if (stompChatManager != null) {
|
||||||
stompChatManager.clearConversationSubscription();
|
stompChatManager.clearConversationSubscription();
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ import com.google.gson.Gson;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import okhttp3.MediaType;
|
import okhttp3.MediaType;
|
||||||
@@ -61,6 +63,7 @@ public class ProfileFragment extends Fragment {
|
|||||||
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
|
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
|
||||||
private Uri photoUri;
|
private Uri photoUri;
|
||||||
private UserDTO currentUser;
|
private UserDTO currentUser;
|
||||||
|
private boolean hasImage = false;
|
||||||
|
|
||||||
//Initialize the launchers for camera and gallery
|
//Initialize the launchers for camera and gallery
|
||||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||||
@@ -149,12 +152,20 @@ public class ProfileFragment extends Fragment {
|
|||||||
//Set up listeners for the buttons
|
//Set up listeners for the buttons
|
||||||
//Change photo button
|
//Change photo button
|
||||||
btnChangePhoto.setOnClickListener(v -> {
|
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
|
//Show alert dialog to user to select from gallery or camera
|
||||||
new AlertDialog.Builder(requireContext())
|
new AlertDialog.Builder(requireContext())
|
||||||
.setTitle("Change Profile Photo")
|
.setTitle("Change Profile Photo")
|
||||||
//set the options for the alert dialog
|
//set the options for the alert dialog
|
||||||
.setItems(new String[]{"Take Photo", "Choose from Gallery"}, (dialog, which) -> {
|
.setItems(options.toArray(new String[0]), (dialog, which) -> {
|
||||||
if (which == 0) {
|
String selected = options.get(which);
|
||||||
|
if (selected.equals("Take Photo")) {
|
||||||
// Choose Camera
|
// Choose Camera
|
||||||
//Checks if the user has granted the camera permission already
|
//Checks if the user has granted the camera permission already
|
||||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||||
@@ -164,11 +175,13 @@ public class ProfileFragment extends Fragment {
|
|||||||
//otherwise request the permission
|
//otherwise request the permission
|
||||||
permissionLauncher.launch(Manifest.permission.CAMERA);
|
permissionLauncher.launch(Manifest.permission.CAMERA);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (selected.equals("Choose from Gallery")) {
|
||||||
// Choose Gallery
|
// Choose Gallery
|
||||||
Intent intent = new Intent(Intent.ACTION_PICK,
|
Intent intent = new Intent(Intent.ACTION_PICK,
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||||
galleryLauncher.launch(intent);
|
galleryLauncher.launch(intent);
|
||||||
|
} else if (selected.equals("Remove Photo")) {
|
||||||
|
deleteAvatar();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.show();
|
.show();
|
||||||
@@ -294,9 +307,23 @@ public class ProfileFragment extends Fragment {
|
|||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.placeholder(R.drawable.placeholder)
|
.placeholder(R.drawable.placeholder)
|
||||||
.error(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);
|
.into(imgProfile);
|
||||||
} else {
|
} else {
|
||||||
// load placeholder image if token is null
|
// load placeholder image if token is null
|
||||||
|
hasImage = false;
|
||||||
Glide.with(ProfileFragment.this)
|
Glide.with(ProfileFragment.this)
|
||||||
.load(R.drawable.placeholder)
|
.load(R.drawable.placeholder)
|
||||||
.into(imgProfile);
|
.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
|
// Helper function to create a temporary File object from a Uri for uploading the avatar
|
||||||
private File getFileFromUri(Uri uri) {
|
private File getFileFromUri(Uri uri) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.petstoremobile.fragments.listfragments;
|
package com.example.petstoremobile.fragments.listfragments;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -29,10 +30,21 @@ import com.example.petstoremobile.dtos.PageResponse;
|
|||||||
import com.example.petstoremobile.dtos.PetDTO;
|
import com.example.petstoremobile.dtos.PetDTO;
|
||||||
import com.example.petstoremobile.fragments.ListFragment;
|
import com.example.petstoremobile.fragments.ListFragment;
|
||||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.AppointmentDetailFragment;
|
import com.example.petstoremobile.fragments.listfragments.detailfragments.AppointmentDetailFragment;
|
||||||
|
import com.example.petstoremobile.utils.EventDecorator;
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
|
import com.prolificinteractive.materialcalendarview.CalendarDay;
|
||||||
|
import com.prolificinteractive.materialcalendarview.CalendarMode;
|
||||||
|
import com.prolificinteractive.materialcalendarview.MaterialCalendarView;
|
||||||
|
import com.prolificinteractive.materialcalendarview.OnDateSelectedListener;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
@@ -50,6 +62,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
private EditText etSearch;
|
private EditText etSearch;
|
||||||
private ImageButton hamburger;
|
private ImageButton hamburger;
|
||||||
|
private ImageButton btnToggleCalendarMode;
|
||||||
|
private MaterialCalendarView calendarView;
|
||||||
|
private CalendarDay selectedCalendarDay;
|
||||||
|
private boolean isMonthMode = false;
|
||||||
|
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
@@ -58,10 +75,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
|
|
||||||
api = RetrofitClient.getAppointmentApi(requireContext());
|
api = RetrofitClient.getAppointmentApi(requireContext());
|
||||||
hamburger = view.findViewById(R.id.btnHamburger);
|
hamburger = view.findViewById(R.id.btnHamburger);
|
||||||
|
calendarView = view.findViewById(R.id.calendarView);
|
||||||
|
btnToggleCalendarMode = view.findViewById(R.id.btnToggleCalendarMode);
|
||||||
|
|
||||||
setupRecyclerView(view);
|
setupRecyclerView(view);
|
||||||
setupSearch(view);
|
setupSearch(view);
|
||||||
setupSwipeRefresh(view);
|
setupSwipeRefresh(view);
|
||||||
|
setupCalendar();
|
||||||
loadAppointmentData();
|
loadAppointmentData();
|
||||||
loadPets();
|
loadPets();
|
||||||
loadServices();
|
loadServices();
|
||||||
@@ -76,9 +96,60 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
listFragment.openDrawer();
|
listFragment.openDrawer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode());
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle Calendar Mode from week to month and other way around
|
||||||
|
private void toggleCalendarMode() {
|
||||||
|
isMonthMode = !isMonthMode;
|
||||||
|
calendarView.state().edit()
|
||||||
|
.setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS)
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupCalendar() {
|
||||||
|
calendarView.setOnDateChangedListener(new OnDateSelectedListener() {
|
||||||
|
@Override
|
||||||
|
public void onDateSelected(@NonNull MaterialCalendarView widget, @NonNull CalendarDay date, boolean selected) {
|
||||||
|
if (selected) {
|
||||||
|
if (date.equals(selectedCalendarDay)) {
|
||||||
|
selectedCalendarDay = null;
|
||||||
|
calendarView.clearSelection();
|
||||||
|
} else {
|
||||||
|
selectedCalendarDay = date;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedCalendarDay = null;
|
||||||
|
}
|
||||||
|
filterAppointments(etSearch.getText().toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Set indicators for dates with appointments on the calendar
|
||||||
|
private void updateCalendarDecorators() {
|
||||||
|
HashSet<CalendarDay> datesWithAppointments = new HashSet<>();
|
||||||
|
for (AppointmentDTO appointment : appointmentList) {
|
||||||
|
try {
|
||||||
|
//Get the appointment date
|
||||||
|
Date date = dateFormat.parse(appointment.getAppointmentDate());
|
||||||
|
//if the date is not null, add it to the hashset
|
||||||
|
if (date != null) {
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
cal.setTime(date);
|
||||||
|
datesWithAppointments.add(CalendarDay.from(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)));
|
||||||
|
}
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Log.e("AppointmentFragment", "Error parsing date: " + appointment.getAppointmentDate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//update the indicators to the calendar
|
||||||
|
calendarView.removeDecorators();
|
||||||
|
calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments));
|
||||||
|
}
|
||||||
|
|
||||||
private void setupSearch(View view) {
|
private void setupSearch(View view) {
|
||||||
etSearch = view.findViewById(R.id.etSearchAppointment);
|
etSearch = view.findViewById(R.id.etSearchAppointment);
|
||||||
etSearch.addTextChangedListener(new TextWatcher() {
|
etSearch.addTextChangedListener(new TextWatcher() {
|
||||||
@@ -99,16 +170,25 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
|
|
||||||
private void filterAppointments(String query) {
|
private void filterAppointments(String query) {
|
||||||
filteredList.clear();
|
filteredList.clear();
|
||||||
if (query.isEmpty()) {
|
String lowerQuery = query.toLowerCase();
|
||||||
filteredList.addAll(appointmentList);
|
|
||||||
} else {
|
String selectedDateString = null;
|
||||||
String lower = query.toLowerCase();
|
if (selectedCalendarDay != null) {
|
||||||
for (AppointmentDTO a : appointmentList) {
|
selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d",
|
||||||
if ((a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lower))
|
selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay());
|
||||||
|| (a.getServiceType() != null && a.getServiceType().toLowerCase().contains(lower))
|
}
|
||||||
|| (a.getPetName() != null && a.getPetName().toLowerCase().contains(lower))) {
|
|
||||||
filteredList.add(a);
|
for (AppointmentDTO a : appointmentList) {
|
||||||
}
|
boolean matchesSearch = query.isEmpty() ||
|
||||||
|
(a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lowerQuery)) ||
|
||||||
|
(a.getServiceType() != null && a.getServiceType().toLowerCase().contains(lowerQuery)) ||
|
||||||
|
(a.getPetName() != null && a.getPetName().toLowerCase().contains(lowerQuery));
|
||||||
|
|
||||||
|
boolean matchesDate = (selectedDateString == null) ||
|
||||||
|
(a.getAppointmentDate() != null && a.getAppointmentDate().equals(selectedDateString));
|
||||||
|
|
||||||
|
if (matchesSearch && matchesDate) {
|
||||||
|
filteredList.add(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
@@ -141,17 +221,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
if (lf != null) lf.loadFragment(detailFragment);
|
if (lf != null) lf.loadFragment(detailFragment);
|
||||||
}
|
}
|
||||||
public void onAppointmentSaved(int position, AppointmentDTO appointment) {
|
public void onAppointmentSaved(int position, AppointmentDTO appointment) {
|
||||||
if (position == -1) {
|
loadAppointmentData();
|
||||||
appointmentList.add(appointment);
|
|
||||||
} else {
|
|
||||||
appointmentList.set(position, appointment);
|
|
||||||
}
|
|
||||||
filterAppointments(etSearch.getText().toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onAppointmentDeleted(int position) {
|
public void onAppointmentDeleted(int position) {
|
||||||
appointmentList.remove(position);
|
loadAppointmentData();
|
||||||
filterAppointments(etSearch.getText().toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -162,7 +236,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
private void loadAppointmentData() {
|
private void loadAppointmentData() {
|
||||||
if (swipeRefreshLayout != null)
|
if (swipeRefreshLayout != null)
|
||||||
swipeRefreshLayout.setRefreshing(true);
|
swipeRefreshLayout.setRefreshing(true);
|
||||||
api.getAllAppointments(0, 100).enqueue(new Callback<PageResponse<AppointmentDTO>>() {
|
api.getAllAppointments(0, 500).enqueue(new Callback<PageResponse<AppointmentDTO>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<PageResponse<AppointmentDTO>> call,
|
public void onResponse(Call<PageResponse<AppointmentDTO>> call,
|
||||||
Response<PageResponse<AppointmentDTO>> response) {
|
Response<PageResponse<AppointmentDTO>> response) {
|
||||||
@@ -171,6 +245,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
appointmentList.clear();
|
appointmentList.clear();
|
||||||
appointmentList.addAll(response.body().getContent());
|
appointmentList.addAll(response.body().getContent());
|
||||||
|
updateCalendarDecorators();
|
||||||
filterAppointments(etSearch != null ? etSearch.getText().toString() : "");
|
filterAppointments(etSearch != null ? etSearch.getText().toString() : "");
|
||||||
} else {
|
} else {
|
||||||
Log.e("AppointmentFragment", "Error: " + response.message());
|
Log.e("AppointmentFragment", "Error: " + response.message());
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
|||||||
setupRecyclerView(view);
|
setupRecyclerView(view);
|
||||||
setupSearch(view);
|
setupSearch(view);
|
||||||
setupSwipeRefresh(view);
|
setupSwipeRefresh(view);
|
||||||
|
|
||||||
loadProducts();
|
loadProducts();
|
||||||
|
|
||||||
FloatingActionButton fab = view.findViewById(R.id.fabAddProduct);
|
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 beforeTextChanged(CharSequence s, int a, int b, int c) {}
|
||||||
public void afterTextChanged(Editable s) {}
|
public void afterTextChanged(Editable s) {}
|
||||||
public void onTextChanged(CharSequence s, int a, int b, int c) {
|
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);
|
swipeRefresh.setOnRefreshListener(this::loadProducts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void filter(String query) {
|
private void filter() {
|
||||||
|
String query = etSearch.getText().toString().toLowerCase();
|
||||||
|
|
||||||
filteredList.clear();
|
filteredList.clear();
|
||||||
if (query.isEmpty()) {
|
for (ProductDTO p : productList) {
|
||||||
filteredList.addAll(productList);
|
boolean matchesSearch = query.isEmpty() ||
|
||||||
} else {
|
(p.getProdName() != null && p.getProdName().toLowerCase().contains(query)) ||
|
||||||
String lower = query.toLowerCase();
|
(p.getCategoryName() != null && p.getCategoryName().toLowerCase().contains(query)) ||
|
||||||
for (ProductDTO p : productList) {
|
(p.getProdDesc() != null && p.getProdDesc().toLowerCase().contains(query));
|
||||||
if ((p.getProdName() != null && p.getProdName().toLowerCase().contains(lower))
|
|
||||||
|| (p.getCategoryName() != null && p.getCategoryName().toLowerCase().contains(lower))) {
|
if (matchesSearch) {
|
||||||
filteredList.add(p);
|
filteredList.add(p);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
@@ -99,7 +101,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
|||||||
if (r.isSuccessful() && r.body() != null) {
|
if (r.isSuccessful() && r.body() != null) {
|
||||||
productList.clear();
|
productList.clear();
|
||||||
productList.addAll(r.body().getContent());
|
productList.addAll(r.body().getContent());
|
||||||
filter(etSearch != null ? etSearch.getText().toString() : "");
|
filter();
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(getContext(), "Failed to load products",
|
Toast.makeText(getContext(), "Failed to load products",
|
||||||
Toast.LENGTH_SHORT).show();
|
Toast.LENGTH_SHORT).show();
|
||||||
|
|||||||
@@ -1,19 +1,37 @@
|
|||||||
package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
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.os.Bundle;
|
||||||
|
import android.provider.MediaStore;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.*;
|
import android.view.*;
|
||||||
import android.widget.*;
|
import android.widget.*;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.content.FileProvider;
|
||||||
import androidx.fragment.app.Fragment;
|
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.R;
|
||||||
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
|
import com.example.petstoremobile.adapters.BlackTextArrayAdapter;
|
||||||
import com.example.petstoremobile.api.*;
|
import com.example.petstoremobile.api.*;
|
||||||
import com.example.petstoremobile.dtos.*;
|
import com.example.petstoremobile.dtos.*;
|
||||||
import com.example.petstoremobile.fragments.ListFragment;
|
import com.example.petstoremobile.fragments.ListFragment;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.MultipartBody;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
import retrofit2.*;
|
import retrofit2.*;
|
||||||
|
|
||||||
public class ProductDetailFragment extends Fragment {
|
public class ProductDetailFragment extends Fragment {
|
||||||
@@ -22,12 +40,59 @@ public class ProductDetailFragment extends Fragment {
|
|||||||
private EditText etProductName, etProductDesc, etProductPrice;
|
private EditText etProductName, etProductDesc, etProductPrice;
|
||||||
private Spinner spinnerCategory;
|
private Spinner spinnerCategory;
|
||||||
private Button btnSave, btnDelete, btnBack;
|
private Button btnSave, btnDelete, btnBack;
|
||||||
|
private ImageView ivProductImage;
|
||||||
|
|
||||||
private long prodId = -1;
|
private long prodId = -1;
|
||||||
private boolean isEditing = false;
|
private boolean isEditing = false;
|
||||||
private long preselectedCategoryId = -1;
|
private long preselectedCategoryId = -1;
|
||||||
|
private boolean hasImage = false;
|
||||||
|
|
||||||
private List<CategoryDTO> categoryList = new ArrayList<>();
|
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
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||||
@@ -40,6 +105,7 @@ public class ProductDetailFragment extends Fragment {
|
|||||||
btnBack.setOnClickListener(v -> navigateBack());
|
btnBack.setOnClickListener(v -> navigateBack());
|
||||||
btnSave.setOnClickListener(v -> saveProduct());
|
btnSave.setOnClickListener(v -> saveProduct());
|
||||||
btnDelete.setOnClickListener(v -> confirmDelete());
|
btnDelete.setOnClickListener(v -> confirmDelete());
|
||||||
|
ivProductImage.setOnClickListener(v -> showImagePickerDialog());
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +119,71 @@ public class ProductDetailFragment extends Fragment {
|
|||||||
btnSave = v.findViewById(R.id.btnSaveProduct);
|
btnSave = v.findViewById(R.id.btnSaveProduct);
|
||||||
btnDelete = v.findViewById(R.id.btnDeleteProduct);
|
btnDelete = v.findViewById(R.id.btnDeleteProduct);
|
||||||
btnBack = v.findViewById(R.id.btnProductBack);
|
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() {
|
private void loadCategories() {
|
||||||
@@ -92,6 +223,7 @@ public class ProductDetailFragment extends Fragment {
|
|||||||
isEditing = true;
|
isEditing = true;
|
||||||
prodId = a.getLong("prodId");
|
prodId = a.getLong("prodId");
|
||||||
preselectedCategoryId = a.getLong("categoryId", -1);
|
preselectedCategoryId = a.getLong("categoryId", -1);
|
||||||
|
hasImage = true;
|
||||||
|
|
||||||
tvMode.setText("Edit Product");
|
tvMode.setText("Edit Product");
|
||||||
tvProductId.setText("ID: " + prodId);
|
tvProductId.setText("ID: " + prodId);
|
||||||
@@ -100,10 +232,74 @@ public class ProductDetailFragment extends Fragment {
|
|||||||
etProductDesc.setText(a.getString("prodDesc"));
|
etProductDesc.setText(a.getString("prodDesc"));
|
||||||
etProductPrice.setText(a.getString("prodPrice"));
|
etProductPrice.setText(a.getString("prodPrice"));
|
||||||
btnDelete.setVisibility(View.VISIBLE);
|
btnDelete.setVisibility(View.VISIBLE);
|
||||||
|
loadProductImage();
|
||||||
} else {
|
} else {
|
||||||
tvMode.setText("Add Product");
|
tvMode.setText("Add Product");
|
||||||
btnDelete.setVisibility(View.GONE);
|
btnDelete.setVisibility(View.GONE);
|
||||||
tvProductId.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);
|
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());
|
ProductApi api = RetrofitClient.getProductApi(requireContext());
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
api.updateProduct(prodId, dto).enqueue(simpleCallback("Updated"));
|
api.updateProduct(prodId, dto).enqueue(simpleCallback("Updated"));
|
||||||
} else {
|
} 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();
|
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else {
|
} else {
|
||||||
try {
|
Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show();
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public void onFailure(Call<ProductDTO> c, Throwable t) {
|
public void onFailure(Call<ProductDTO> c, Throwable t) {
|
||||||
Log.e("PRODUCT_SAVE", "Failure: " + t.getMessage());
|
|
||||||
Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show();
|
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.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import okhttp3.MediaType;
|
import okhttp3.MediaType;
|
||||||
@@ -52,6 +54,7 @@ public class PetProfileFragment extends Fragment {
|
|||||||
private ImageView imgPet;
|
private ImageView imgPet;
|
||||||
private Uri photoUri;
|
private Uri photoUri;
|
||||||
private int petId;
|
private int petId;
|
||||||
|
private boolean hasImage = false;
|
||||||
|
|
||||||
// launchers for camera and gallery
|
// launchers for camera and gallery
|
||||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||||
@@ -162,10 +165,18 @@ public class PetProfileFragment extends Fragment {
|
|||||||
|
|
||||||
//Make change photo button ask user to select a new photo
|
//Make change photo button ask user to select a new photo
|
||||||
btnChangePhoto.setOnClickListener(v -> {
|
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())
|
new AlertDialog.Builder(requireContext())
|
||||||
.setTitle("Change Pet Photo")
|
.setTitle("Change Pet Photo")
|
||||||
.setItems(new String[]{"Take Photo", "Choose from Gallery"}, (dialog, which) -> {
|
.setItems(options.toArray(new String[0]), (dialog, which) -> {
|
||||||
if (which == 0) {
|
String selected = options.get(which);
|
||||||
|
if (selected.equals("Take Photo")) {
|
||||||
// Choose Camera
|
// Choose Camera
|
||||||
//Checks if the user has granted the camera permission already
|
//Checks if the user has granted the camera permission already
|
||||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||||
@@ -175,9 +186,11 @@ public class PetProfileFragment extends Fragment {
|
|||||||
//otherwise request the permission
|
//otherwise request the permission
|
||||||
permissionLauncher.launch(Manifest.permission.CAMERA);
|
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);
|
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||||
galleryLauncher.launch(intent);
|
galleryLauncher.launch(intent);
|
||||||
|
} else if (selected.equals("Remove Photo")) {
|
||||||
|
deletePetImage();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.show();
|
.show();
|
||||||
@@ -196,6 +209,19 @@ public class PetProfileFragment extends Fragment {
|
|||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.placeholder(R.drawable.placeholder)
|
.placeholder(R.drawable.placeholder)
|
||||||
.error(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);
|
.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
|
// Helper function to create a temporary File object from a Uri for uploading
|
||||||
private File getFileFromUri(Uri uri) {
|
private File getFileFromUri(Uri uri) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ public class Message {
|
|||||||
private String content;
|
private String content;
|
||||||
private String timestamp;
|
private String timestamp;
|
||||||
private Boolean isRead;
|
private Boolean isRead;
|
||||||
|
private String attachmentUrl;
|
||||||
|
private String attachmentName;
|
||||||
|
private String attachmentType;
|
||||||
|
|
||||||
public Message() {}
|
public Message() {}
|
||||||
|
|
||||||
@@ -33,4 +36,13 @@ public class Message {
|
|||||||
|
|
||||||
public Boolean getIsRead() { return isRead; }
|
public Boolean getIsRead() { return isRead; }
|
||||||
public void setIsRead(Boolean isRead) { this.isRead = isRead; }
|
public void setIsRead(Boolean isRead) { this.isRead = isRead; }
|
||||||
|
|
||||||
|
public String getAttachmentUrl() { return attachmentUrl; }
|
||||||
|
public void setAttachmentUrl(String attachmentUrl) { this.attachmentUrl = attachmentUrl; }
|
||||||
|
|
||||||
|
public String getAttachmentName() { return attachmentName; }
|
||||||
|
public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; }
|
||||||
|
|
||||||
|
public String getAttachmentType() { return attachmentType; }
|
||||||
|
public void setAttachmentType(String attachmentType) { this.attachmentType = attachmentType; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.example.petstoremobile.utils;
|
||||||
|
|
||||||
|
import com.prolificinteractive.materialcalendarview.CalendarDay;
|
||||||
|
import com.prolificinteractive.materialcalendarview.DayViewDecorator;
|
||||||
|
import com.prolificinteractive.materialcalendarview.DayViewFacade;
|
||||||
|
import com.prolificinteractive.materialcalendarview.spans.DotSpan;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
public class EventDecorator implements DayViewDecorator {
|
||||||
|
|
||||||
|
private final int color;
|
||||||
|
private final HashSet<CalendarDay> dates;
|
||||||
|
|
||||||
|
public EventDecorator(int color, Collection<CalendarDay> dates) {
|
||||||
|
this.color = color;
|
||||||
|
this.dates = new HashSet<>(dates);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldDecorate(CalendarDay day) {
|
||||||
|
return dates.contains(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void decorate(DayViewFacade view) {
|
||||||
|
view.addSpan(new DotSpan(8, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
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 |
@@ -29,15 +29,35 @@
|
|||||||
android:contentDescription="Open menu"/>
|
android:contentDescription="Open menu"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Appointments"
|
android:text="Appointments"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold"/>
|
android:textStyle="bold"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnToggleCalendarMode"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:src="@android:drawable/ic_menu_today"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
app:tint="@color/white"
|
||||||
|
android:contentDescription="Toggle Calendar Mode"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.prolificinteractive.materialcalendarview.MaterialCalendarView
|
||||||
|
android:id="@+id/calendarView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
app:mcv_showOtherDates="all"
|
||||||
|
app:mcv_selectionColor="@color/accent_blue"
|
||||||
|
app:mcv_calendarMode="week"
|
||||||
|
app:mcv_tileHeight="40dp" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/etSearchAppointment"
|
android:id="@+id/etSearchAppointment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -49,12 +49,59 @@
|
|||||||
android:clipToPadding="false" />
|
android:clipToPadding="false" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutAttachmentPreview"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
|
android:background="#E0E0E0"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivPreview"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvPreviewName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="middle"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textColor="@color/text_dark"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnRemoveAttachment"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="Remove attachment"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
android:background="@color/white">
|
android:background="@color/white">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnAttach"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:src="@android:drawable/ic_menu_add"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="Attach file"
|
||||||
|
android:layout_marginEnd="4dp"/>
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/etMessage"
|
android:id="@+id/etMessage"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|||||||
@@ -64,6 +64,32 @@
|
|||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
android:layout_marginBottom="8dp"/>
|
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 -->
|
<!-- Product Name -->
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -6,14 +6,38 @@
|
|||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:gravity="start">
|
android:gravity="start">
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/tvMessageContent"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
android:background="@drawable/bg_message_received"
|
android:background="@drawable/bg_message_received"
|
||||||
android:padding="12dp"
|
android:padding="8dp"
|
||||||
android:text="Received message"
|
android:maxWidth="300dp">
|
||||||
android:textColor="@color/text_dark"
|
|
||||||
android:maxWidth="300dp" />
|
<ImageView
|
||||||
|
android:id="@+id/ivAttachment"
|
||||||
|
android:layout_width="200dp"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_marginBottom="4dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvAttachmentName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/text_dark"
|
||||||
|
android:textStyle="italic"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_marginBottom="4dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvMessageContent"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Received message"
|
||||||
|
android:textColor="@color/text_dark" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -6,14 +6,38 @@
|
|||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:gravity="end">
|
android:gravity="end">
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/tvMessageContent"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
android:background="@drawable/bg_message_sent"
|
android:background="@drawable/bg_message_sent"
|
||||||
android:padding="12dp"
|
android:padding="8dp"
|
||||||
android:text="Sent message"
|
android:maxWidth="300dp">
|
||||||
android:textColor="@color/white"
|
|
||||||
android:maxWidth="300dp" />
|
<ImageView
|
||||||
|
android:id="@+id/ivAttachment"
|
||||||
|
android:layout_width="200dp"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_marginBottom="4dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvAttachmentName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textStyle="italic"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_marginBottom="4dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvMessageContent"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Sent message"
|
||||||
|
android:textColor="@color/white" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
@@ -9,56 +10,83 @@
|
|||||||
android:paddingBottom="16dp"
|
android:paddingBottom="16dp"
|
||||||
android:background="@color/white">
|
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
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical">
|
||||||
android:layout_marginTop="8dp">
|
|
||||||
|
|
||||||
<TextView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:id="@+id/tvProductPrice"
|
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_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="$0.00"
|
android:orientation="vertical">
|
||||||
android:textColor="@color/accent_coral"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
|
<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>
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|||||||
# Android operating system, and which are packaged with your app's APK
|
# Android operating system, and which are packaged with your app's APK
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
# Automatically convert third-party libraries to use AndroidX
|
||||||
|
android.enableJetifier=true
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
# thereby reducing the size of the R class for that library
|
# thereby reducing the size of the R class for that library
|
||||||
|
|||||||
Reference in New Issue
Block a user