Merge pull request #316 from RecentRunner/android-desktop-parity

add pet image support
This commit was merged in pull request #316.
This commit is contained in:
2026-04-15 16:16:14 -06:00
committed by GitHub
3 changed files with 149 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
package com.example.petstoremobile.fragments.listfragments.detailfragments; package com.example.petstoremobile.fragments.listfragments.detailfragments;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -16,7 +17,10 @@ import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
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.PetApi;
import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentPetDetailBinding; import com.example.petstoremobile.databinding.FragmentPetDetailBinding;
import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.dtos.DropdownDTO;
@@ -24,18 +28,26 @@ import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.ActivityLogger;
import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DateTimeUtils;
import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.DialogUtils;
import com.example.petstoremobile.utils.FileUtils;
import com.example.petstoremobile.utils.GlideUtils;
import com.example.petstoremobile.utils.ImagePickerHelper;
import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.InputValidator;
import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.PetDetailViewModel; import com.example.petstoremobile.viewmodels.PetDetailViewModel;
import java.io.File;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
/** /**
* Fragment for displaying and editing pet details. * Fragment for displaying and editing pet details.
@@ -45,14 +57,45 @@ public class PetDetailFragment extends Fragment {
private FragmentPetDetailBinding binding; private FragmentPetDetailBinding binding;
private PetDetailViewModel viewModel; private PetDetailViewModel viewModel;
private ImagePickerHelper imagePickerHelper;
private boolean isUpdatingUI = false; private boolean isUpdatingUI = false;
private boolean hasImage = false;
private boolean isImageChanged = false;
private boolean isImageRemoved = false;
private Uri photoUri;
@Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager; @Inject TokenManager tokenManager;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PetDetailViewModel.class); viewModel = new ViewModelProvider(this).get(PetDetailViewModel.class);
imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
@Override
public void onImagePicked(Uri uri) {
photoUri = uri;
Glide.with(PetDetailFragment.this)
.load(uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.into(binding.ivPetImage);
hasImage = true;
isImageChanged = true;
isImageRemoved = false;
}
@Override
public void onImageRemoved() {
photoUri = null;
hasImage = false;
isImageChanged = false;
isImageRemoved = true;
binding.ivPetImage.setImageResource(R.drawable.placeholder2);
}
});
} }
@Override @Override
@@ -74,6 +117,7 @@ public class PetDetailFragment extends Fragment {
binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnBack.setOnClickListener(v -> navigateBack());
binding.btnSavePet.setOnClickListener(v -> savePet()); binding.btnSavePet.setOnClickListener(v -> savePet());
binding.btnDeletePet.setOnClickListener(v -> deletePet()); binding.btnDeletePet.setOnClickListener(v -> deletePet());
binding.ivPetImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Pet Image", hasImage));
} }
private void observeViewModel() { private void observeViewModel() {
@@ -197,14 +241,18 @@ public class PetDetailFragment extends Fragment {
if (resource == null) return; if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING); setLoading(resource.status == Resource.Status.LOADING);
if (resource.status == Resource.Status.SUCCESS) { if (resource.status == Resource.Status.SUCCESS) {
if (resource.data != null) {
viewModel.setPetId(resource.data.getPetId());
}
String msg;
if (viewModel.isEditing()) { if (viewModel.isEditing()) {
ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) viewModel.getPetId()); ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) viewModel.getPetId());
Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); msg = "Pet updated successfully!";
} else { } else {
ActivityLogger.log(requireContext(), "Added new Pet: " + name); ActivityLogger.log(requireContext(), "Added new Pet: " + name);
Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show(); msg = "Pet added successfully!";
} }
navigateToPetList(); performPendingImageActions(msg);
} else if (resource.status == Resource.Status.ERROR) { } else if (resource.status == Resource.Status.ERROR) {
Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show();
} }
@@ -239,10 +287,75 @@ public class PetDetailFragment extends Fragment {
if (getArguments() != null && getArguments().containsKey("petId")) { if (getArguments() != null && getArguments().containsKey("petId")) {
viewModel.setPetId(getArguments().getLong("petId")); viewModel.setPetId(getArguments().getLong("petId"));
loadPetData(); loadPetData();
loadPetImage();
return; return;
} }
viewModel.setPetId(-1); viewModel.setPetId(-1);
hasImage = false;
}
private void loadPetImage() {
String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, viewModel.getPetId());
String token = tokenManager.getToken();
GlideUtils.loadImageWithToken(requireContext(), binding.ivPetImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() {
@Override
public void onResourceReady() {
hasImage = true;
}
@Override
public void onLoadFailed() {
hasImage = false;
}
});
}
private void performPendingImageActions(String successMsg) {
if (isImageRemoved) {
viewModel.deletePetImage().observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), successMsg + " (but image removal failed)", Toast.LENGTH_SHORT).show();
}
navigateToPetList();
}
});
} else if (isImageChanged && photoUri != null) {
uploadPetImageAndNavigate(photoUri, successMsg);
} else {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
navigateToPetList();
}
}
private void uploadPetImageAndNavigate(Uri uri, String successMsg) {
File file = FileUtils.getFileFromUri(requireContext(), uri);
if (file == null) {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
navigateToPetList();
return;
}
RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri)));
MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile);
viewModel.uploadPetImage(body).observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
setLoading(resource.status == Resource.Status.LOADING);
if (resource.status != Resource.Status.LOADING) {
if (resource.status == Resource.Status.SUCCESS) {
Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), successMsg + " (but image upload failed)", Toast.LENGTH_SHORT).show();
}
navigateToPetList();
}
});
} }
private void loadPetData() { private void loadPetData() {

View File

@@ -239,6 +239,14 @@ public class PetDetailViewModel extends ViewModel {
return petRepository.deletePet(petId); return petRepository.deletePet(petId);
} }
public LiveData<Resource<Void>> uploadPetImage(okhttp3.MultipartBody.Part image) {
return petRepository.uploadPetImage(petId, image);
}
public LiveData<Resource<Void>> deletePetImage() {
return petRepository.deletePetImage(petId);
}
public LiveData<List<DropdownDTO>> getCustomerList() { public LiveData<List<DropdownDTO>> getCustomerList() {
return customerList; return customerList;
} }

View File

@@ -70,6 +70,31 @@
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginBottom="8dp"/> android:layout_marginBottom="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pet 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/ivPetImage"
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>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"