add desktop pet and product images

This commit is contained in:
2026-03-27 10:07:37 -06:00
parent 2fb409f0d9
commit 8c6a53250a
14 changed files with 402 additions and 23 deletions

View File

@@ -15,15 +15,17 @@ public class ProductDTO {
private SimpleIntegerProperty categoryId; //used for edit and delete private SimpleIntegerProperty categoryId; //used for edit and delete
private SimpleStringProperty categoryName; private SimpleStringProperty categoryName;
private SimpleStringProperty prodDesc; private SimpleStringProperty prodDesc;
private SimpleStringProperty imageUrl;
//constructor //constructor
public ProductDTO(int prodId, String prodName, double prodPrice, int categoryId, String categoryName, String prodDesc) { public ProductDTO(int prodId, String prodName, double prodPrice, int categoryId, String categoryName, String prodDesc, String imageUrl) {
this.prodId = new SimpleIntegerProperty(prodId); this.prodId = new SimpleIntegerProperty(prodId);
this.prodName = new SimpleStringProperty(prodName); this.prodName = new SimpleStringProperty(prodName);
this.prodPrice = new SimpleDoubleProperty(prodPrice); this.prodPrice = new SimpleDoubleProperty(prodPrice);
this.categoryId = new SimpleIntegerProperty(categoryId); this.categoryId = new SimpleIntegerProperty(categoryId);
this.categoryName = new SimpleStringProperty(categoryName); this.categoryName = new SimpleStringProperty(categoryName);
this.prodDesc = new SimpleStringProperty(prodDesc); this.prodDesc = new SimpleStringProperty(prodDesc);
this.imageUrl = new SimpleStringProperty(imageUrl);
} }
//getter and setters //getter and setters
@@ -99,6 +101,18 @@ public class ProductDTO {
this.categoryId.set(categoryId); this.categoryId.set(categoryId);
} }
public String getImageUrl() {
return imageUrl.get();
}
public SimpleStringProperty imageUrlProperty() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl.set(imageUrl);
}
/** /**
* Converts DTO into product for editing and deleting * Converts DTO into product for editing and deleting
* @return * @return

View File

@@ -67,7 +67,7 @@ public class ApiClient {
} else if (statusCode == 403) { } else if (statusCode == 403) {
throw new RuntimeException("Access restricted. You don't have permission to perform this action."); throw new RuntimeException("Access restricted. You don't have permission to perform this action.");
} else if (statusCode == 404) { } else if (statusCode == 404) {
throw new RuntimeException("Avatar not found."); throw new RuntimeException("File not found.");
} else { } else {
throw new RuntimeException("Request failed with status " + statusCode); throw new RuntimeException("Request failed with status " + statusCode);
} }

View File

@@ -9,6 +9,7 @@ import org.example.petshopdesktop.api.dto.pet.PetResponse;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List; import java.util.List;
public class PetApi { public class PetApi {
@@ -47,6 +48,18 @@ public class PetApi {
return apiClient.put("/api/v1/pets/" + id, request, PetResponse.class); return apiClient.put("/api/v1/pets/" + id, request, PetResponse.class);
} }
public PetResponse uploadPetImage(Long id, Path imagePath) throws Exception {
return apiClient.postMultipart("/api/v1/pets/" + id + "/image", "image", imagePath, PetResponse.class);
}
public void deletePetImage(Long id) throws Exception {
apiClient.delete("/api/v1/pets/" + id + "/image");
}
public byte[] getPetImage(Long id) throws Exception {
return apiClient.getBytes("/api/v1/pets/" + id + "/image");
}
public void deletePets(List<Long> ids) throws Exception { public void deletePets(List<Long> ids) throws Exception {
apiClient.deleteWithBody("/api/v1/pets", new BulkDeleteRequest(ids)); apiClient.deleteWithBody("/api/v1/pets", new BulkDeleteRequest(ids));
} }

View File

@@ -9,6 +9,7 @@ import org.example.petshopdesktop.api.dto.product.ProductResponse;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List; import java.util.List;
public class ProductApi { public class ProductApi {
@@ -47,6 +48,18 @@ public class ProductApi {
return apiClient.put("/api/v1/products/" + id, request, ProductResponse.class); return apiClient.put("/api/v1/products/" + id, request, ProductResponse.class);
} }
public ProductResponse uploadProductImage(Long id, Path imagePath) throws Exception {
return apiClient.postMultipart("/api/v1/products/" + id + "/image", "image", imagePath, ProductResponse.class);
}
public void deleteProductImage(Long id) throws Exception {
apiClient.delete("/api/v1/products/" + id + "/image");
}
public byte[] getProductImage(Long id) throws Exception {
return apiClient.getBytes("/api/v1/products/" + id + "/image");
}
public void deleteProducts(List<Long> ids) throws Exception { public void deleteProducts(List<Long> ids) throws Exception {
apiClient.deleteWithBody("/api/v1/products", new BulkDeleteRequest(ids)); apiClient.deleteWithBody("/api/v1/products", new BulkDeleteRequest(ids));
} }

View File

@@ -6,9 +6,12 @@ import javafx.collections.ObservableList;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.pet.PetResponse; import org.example.petshopdesktop.api.dto.pet.PetResponse;
@@ -16,6 +19,7 @@ import org.example.petshopdesktop.api.endpoints.PetApi;
import org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController; import org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController;
import org.example.petshopdesktop.models.Pet; import org.example.petshopdesktop.models.Pet;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
import org.example.petshopdesktop.util.DesktopImageSupport;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@@ -42,6 +46,9 @@ public class PetController {
@FXML @FXML
private TableColumn<Pet, Integer> colPetId; private TableColumn<Pet, Integer> colPetId;
@FXML
private TableColumn<Pet, String> colPetImage;
@FXML @FXML
private TableColumn<Pet, String> colPetName; private TableColumn<Pet, String> colPetName;
@@ -134,12 +141,14 @@ public class PetController {
tvPets.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); tvPets.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
colPetId.setCellValueFactory(new PropertyValueFactory<Pet,Integer>("petId")); colPetId.setCellValueFactory(new PropertyValueFactory<Pet,Integer>("petId"));
colPetImage.setCellValueFactory(new PropertyValueFactory<Pet, String>("imageUrl"));
colPetName.setCellValueFactory(new PropertyValueFactory<Pet,String>("petName")); colPetName.setCellValueFactory(new PropertyValueFactory<Pet,String>("petName"));
colPetSpecies.setCellValueFactory(new PropertyValueFactory<Pet,String>("petSpecies")); colPetSpecies.setCellValueFactory(new PropertyValueFactory<Pet,String>("petSpecies"));
colPetBreed.setCellValueFactory(new PropertyValueFactory<Pet,String>("petBreed")); colPetBreed.setCellValueFactory(new PropertyValueFactory<Pet,String>("petBreed"));
colPetAge.setCellValueFactory(new PropertyValueFactory<Pet,Integer>("petAge")); colPetAge.setCellValueFactory(new PropertyValueFactory<Pet,Integer>("petAge"));
colPetStatus.setCellValueFactory(new PropertyValueFactory<Pet,String>("petStatus")); colPetStatus.setCellValueFactory(new PropertyValueFactory<Pet,String>("petStatus"));
colPetPrice.setCellValueFactory(new PropertyValueFactory<Pet,Double>("petPrice")); colPetPrice.setCellValueFactory(new PropertyValueFactory<Pet,Double>("petPrice"));
configureImageColumn(colPetImage);
displayPets(); displayPets();
@@ -262,8 +271,30 @@ public class PetController {
response.getPetBreed(), response.getPetBreed(),
response.getPetAge() != null ? response.getPetAge() : 0, response.getPetAge() != null ? response.getPetAge() : 0,
response.getPetStatus(), response.getPetStatus(),
response.getPetPrice().doubleValue() response.getPetPrice().doubleValue(),
response.getImageUrl()
); );
} }
private void configureImageColumn(TableColumn<Pet, String> column) {
column.setCellFactory(col -> new TableCell<>() {
private final ImageView imageView = new ImageView();
private final StackPane container = new StackPane(imageView);
{
container.setAlignment(Pos.CENTER);
}
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null || item.isBlank()) {
setGraphic(null);
return;
}
DesktopImageSupport.loadImageInto(imageView, item, 48, 48);
setGraphic(container);
}
});
}
} }

View File

@@ -6,9 +6,12 @@ import javafx.collections.ObservableList;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.example.petshopdesktop.DTOs.ProductDTO; import org.example.petshopdesktop.DTOs.ProductDTO;
@@ -16,6 +19,7 @@ import org.example.petshopdesktop.api.dto.product.ProductResponse;
import org.example.petshopdesktop.api.endpoints.ProductApi; import org.example.petshopdesktop.api.endpoints.ProductApi;
import org.example.petshopdesktop.controllers.dialogcontrollers.ProductDialogController; import org.example.petshopdesktop.controllers.dialogcontrollers.ProductDialogController;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
import org.example.petshopdesktop.util.DesktopImageSupport;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@@ -46,6 +50,9 @@ public class ProductController {
@FXML @FXML
private TableColumn<ProductDTO, Integer> colProductId; private TableColumn<ProductDTO, Integer> colProductId;
@FXML
private TableColumn<ProductDTO, String> colProductImage;
@FXML @FXML
private TableColumn<ProductDTO, String> colProductName; private TableColumn<ProductDTO, String> colProductName;
@@ -74,10 +81,12 @@ public class ProductController {
tvProducts.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); tvProducts.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
//set up table columns //set up table columns
colProductId.setCellValueFactory(new PropertyValueFactory<ProductDTO,Integer>("prodId")); colProductId.setCellValueFactory(new PropertyValueFactory<ProductDTO,Integer>("prodId"));
colProductImage.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("imageUrl"));
colProductName.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("prodName")); colProductName.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("prodName"));
colProductPrice.setCellValueFactory(new PropertyValueFactory<ProductDTO,Double>("prodPrice")); colProductPrice.setCellValueFactory(new PropertyValueFactory<ProductDTO,Double>("prodPrice"));
colProductCategory.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("categoryName")); colProductCategory.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("categoryName"));
colProductDesc.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("prodDesc")); colProductDesc.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("prodDesc"));
configureImageColumn(colProductImage);
displayProduct(); displayProduct();
@@ -292,8 +301,30 @@ public class ProductController {
response.getProdPrice().doubleValue(), response.getProdPrice().doubleValue(),
0, 0,
response.getCategoryName(), response.getCategoryName(),
response.getProdDesc() response.getProdDesc(),
response.getImageUrl()
); );
} }
private void configureImageColumn(TableColumn<ProductDTO, String> column) {
column.setCellFactory(col -> new TableCell<>() {
private final ImageView imageView = new ImageView();
private final StackPane container = new StackPane(imageView);
{
container.setAlignment(Pos.CENTER);
}
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null || item.isBlank()) {
setGraphic(null);
return;
}
DesktopImageSupport.loadImageInto(imageView, item, 48, 48);
setGraphic(container);
}
});
}
} }

View File

@@ -6,6 +6,7 @@ import javafx.event.EventHandler;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.example.petshopdesktop.Validator; import org.example.petshopdesktop.Validator;
@@ -14,7 +15,10 @@ import org.example.petshopdesktop.api.dto.pet.PetResponse;
import org.example.petshopdesktop.api.endpoints.PetApi; import org.example.petshopdesktop.api.endpoints.PetApi;
import org.example.petshopdesktop.models.Pet; import org.example.petshopdesktop.models.Pet;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
import org.example.petshopdesktop.util.DesktopImageSupport;
import org.example.petshopdesktop.util.FilePickerSupport;
import java.io.File;
import java.math.BigDecimal; import java.math.BigDecimal;
public class PetDialogController { public class PetDialogController {
@@ -24,6 +28,12 @@ public class PetDialogController {
@FXML @FXML
private Button btnSave; private Button btnSave;
@FXML
private Button btnChangeImage;
@FXML
private Button btnRemoveImage;
@FXML @FXML
private ComboBox<String> cbPetStatus; private ComboBox<String> cbPetStatus;
@@ -33,6 +43,12 @@ public class PetDialogController {
@FXML @FXML
private Label lblPetId; private Label lblPetId;
@FXML
private Label lblImageStatus;
@FXML
private ImageView imgPetPreview;
@FXML @FXML
private TextField txtPetAge; private TextField txtPetAge;
@@ -49,6 +65,9 @@ public class PetDialogController {
private TextField txtPetSpecies; private TextField txtPetSpecies;
private String mode = null; private String mode = null;
private File selectedImageFile;
private String currentImageUrl;
private boolean removeImageRequested;
private ObservableList<String> statusList = FXCollections.observableArrayList( private ObservableList<String> statusList = FXCollections.observableArrayList(
"Available", "Adopted" "Available", "Adopted"
@@ -73,6 +92,10 @@ public class PetDialogController {
closeStage(mouseEvent); closeStage(mouseEvent);
} }
}); });
btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage());
btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage());
refreshImagePreview();
} }
private void buttonSaveClicked(MouseEvent mouseEvent) { private void buttonSaveClicked(MouseEvent mouseEvent) {
@@ -103,7 +126,8 @@ public class PetDialogController {
PetRequest request = buildPetRequest(); PetRequest request = buildPetRequest();
try { try {
if(mode.equals("Add")) { if(mode.equals("Add")) {
PetApi.getInstance().createPet(request); PetResponse response = PetApi.getInstance().createPet(request);
applyImageChanges(response.getPetId());
} else { } else {
String[] parts = lblPetId.getText().split(": "); String[] parts = lblPetId.getText().split(": ");
if (parts.length < 2) { if (parts.length < 2) {
@@ -111,6 +135,7 @@ public class PetDialogController {
} }
Long petId = Long.parseLong(parts[1]); Long petId = Long.parseLong(parts[1]);
PetApi.getInstance().updatePet(petId, request); PetApi.getInstance().updatePet(petId, request);
applyImageChanges(petId);
} }
//tell the user operation was successful //tell the user operation was successful
@@ -175,6 +200,10 @@ public class PetDialogController {
txtPetBreed.setText(pet.getPetBreed()); txtPetBreed.setText(pet.getPetBreed());
txtPetAge.setText(pet.getPetAge() + ""); txtPetAge.setText(pet.getPetAge() + "");
txtPetPrice.setText(pet.getPetPrice() + ""); txtPetPrice.setText(pet.getPetPrice() + "");
currentImageUrl = pet.getImageUrl();
selectedImageFile = null;
removeImageRequested = false;
refreshImagePreview();
//get the right combobox selection //get the right combobox selection
for (String status : cbPetStatus.getItems()) { for (String status : cbPetStatus.getItems()) {
@@ -192,10 +221,67 @@ public class PetDialogController {
lblMode.setText(mode + " Pet"); lblMode.setText(mode + " Pet");
if(mode.equals("Add")) { if(mode.equals("Add")) {
lblPetId.setVisible(false); lblPetId.setVisible(false);
currentImageUrl = null;
selectedImageFile = null;
removeImageRequested = false;
refreshImagePreview();
} }
else if(mode.equals("Edit")) { else if(mode.equals("Edit")) {
lblPetId.setVisible(true); lblPetId.setVisible(true);
refreshImagePreview();
} }
} }
private void handleChangeImage() {
File file = FilePickerSupport.pickImageFile(btnSave.getScene().getWindow());
if (file == null) {
return;
}
selectedImageFile = file;
removeImageRequested = false;
lblImageStatus.setText("Selected: " + file.getName());
DesktopImageSupport.loadImageInto(imgPetPreview, file.toURI().toString(), 120, 120);
btnRemoveImage.setDisable(false);
}
private void handleRemoveImage() {
selectedImageFile = null;
removeImageRequested = true;
currentImageUrl = null;
refreshImagePreview();
}
private void applyImageChanges(Long petId) throws Exception {
if (removeImageRequested) {
try {
PetApi.getInstance().deletePetImage(petId);
} catch (Exception ignored) {
}
}
if (selectedImageFile != null) {
PetApi.getInstance().uploadPetImage(petId, selectedImageFile.toPath());
}
}
private void refreshImagePreview() {
if (imgPetPreview == null || lblImageStatus == null || btnRemoveImage == null) {
return;
}
imgPetPreview.setImage(null);
if (selectedImageFile != null) {
lblImageStatus.setText("Selected: " + selectedImageFile.getName());
DesktopImageSupport.loadImageInto(imgPetPreview, selectedImageFile.toURI().toString(), 120, 120);
btnRemoveImage.setDisable(false);
return;
}
if (currentImageUrl != null && !currentImageUrl.isBlank()) {
lblImageStatus.setText("Current image loaded");
DesktopImageSupport.loadImageInto(imgPetPreview, currentImageUrl, 120, 120);
btnRemoveImage.setDisable(false);
return;
}
lblImageStatus.setText("No image selected");
btnRemoveImage.setDisable(true);
}
} }

View File

@@ -6,16 +6,21 @@ import javafx.event.EventHandler;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.example.petshopdesktop.DTOs.ProductDTO; import org.example.petshopdesktop.DTOs.ProductDTO;
import org.example.petshopdesktop.Validator; import org.example.petshopdesktop.Validator;
import org.example.petshopdesktop.api.dto.common.DropdownOption; import org.example.petshopdesktop.api.dto.common.DropdownOption;
import org.example.petshopdesktop.api.dto.product.ProductRequest; import org.example.petshopdesktop.api.dto.product.ProductRequest;
import org.example.petshopdesktop.api.dto.product.ProductResponse;
import org.example.petshopdesktop.api.endpoints.DropdownApi; import org.example.petshopdesktop.api.endpoints.DropdownApi;
import org.example.petshopdesktop.api.endpoints.ProductApi; import org.example.petshopdesktop.api.endpoints.ProductApi;
import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.ActivityLogger;
import org.example.petshopdesktop.util.DesktopImageSupport;
import org.example.petshopdesktop.util.FilePickerSupport;
import java.io.File;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
@@ -27,6 +32,12 @@ public class ProductDialogController {
@FXML @FXML
private Button btnSave; private Button btnSave;
@FXML
private Button btnChangeImage;
@FXML
private Button btnRemoveImage;
@FXML @FXML
private ComboBox<DropdownOption> cbProdCategory; private ComboBox<DropdownOption> cbProdCategory;
@@ -36,6 +47,12 @@ public class ProductDialogController {
@FXML @FXML
private Label lblProdId; private Label lblProdId;
@FXML
private Label lblImageStatus;
@FXML
private ImageView imgProductPreview;
@FXML @FXML
private TextField txtProdDesc; private TextField txtProdDesc;
@@ -46,6 +63,9 @@ public class ProductDialogController {
private TextField txtProdPrice; private TextField txtProdPrice;
private String mode = null; private String mode = null;
private File selectedImageFile;
private String currentImageUrl;
private boolean removeImageRequested;
/** /**
* Add event listeners to buttons when dialog loads * Add event listeners to buttons when dialog loads
@@ -82,6 +102,10 @@ public class ProductDialogController {
System.out.println("Error loading categories: " + e.getMessage()); System.out.println("Error loading categories: " + e.getMessage());
} }
btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage());
btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage());
refreshImagePreview();
} }
/** /**
@@ -123,7 +147,8 @@ public class ProductDialogController {
request.setProdDesc(txtProdDesc.getText()); request.setProdDesc(txtProdDesc.getText());
if (mode.equals("Add")) { if (mode.equals("Add")) {
ProductApi.getInstance().createProduct(request); ProductResponse response = ProductApi.getInstance().createProduct(request);
applyImageChanges(response.getProdId());
} else { } else {
String[] parts = lblProdId.getText().split(": "); String[] parts = lblProdId.getText().split(": ");
if (parts.length < 2) { if (parts.length < 2) {
@@ -131,6 +156,7 @@ public class ProductDialogController {
} }
Long productId = Long.parseLong(parts[1]); Long productId = Long.parseLong(parts[1]);
ProductApi.getInstance().updateProduct(productId, request); ProductApi.getInstance().updateProduct(productId, request);
applyImageChanges(productId);
} }
Alert alert = new Alert(Alert.AlertType.INFORMATION); Alert alert = new Alert(Alert.AlertType.INFORMATION);
@@ -167,6 +193,10 @@ public class ProductDialogController {
txtProdName.setText(product.getProdName()); txtProdName.setText(product.getProdName());
txtProdDesc.setText(product.getProdDesc()); txtProdDesc.setText(product.getProdDesc());
txtProdPrice.setText(product.getProdPrice() + ""); txtProdPrice.setText(product.getProdPrice() + "");
currentImageUrl = product.getImageUrl();
selectedImageFile = null;
removeImageRequested = false;
refreshImagePreview();
for (DropdownOption category : cbProdCategory.getItems()) { for (DropdownOption category : cbProdCategory.getItems()) {
if(category.getLabel().equals(product.getCategoryName())){ if(category.getLabel().equals(product.getCategoryName())){
@@ -197,10 +227,67 @@ public class ProductDialogController {
lblMode.setText(mode + " Product"); lblMode.setText(mode + " Product");
if(mode.equals("Add")) { if(mode.equals("Add")) {
lblProdId.setVisible(false); lblProdId.setVisible(false);
currentImageUrl = null;
selectedImageFile = null;
removeImageRequested = false;
refreshImagePreview();
} }
else if(mode.equals("Edit")) { else if(mode.equals("Edit")) {
lblProdId.setVisible(true); lblProdId.setVisible(true);
refreshImagePreview();
} }
} }
private void handleChangeImage() {
File file = FilePickerSupport.pickImageFile(btnSave.getScene().getWindow());
if (file == null) {
return;
}
selectedImageFile = file;
removeImageRequested = false;
lblImageStatus.setText("Selected: " + file.getName());
DesktopImageSupport.loadImageInto(imgProductPreview, file.toURI().toString(), 120, 120);
btnRemoveImage.setDisable(false);
}
private void handleRemoveImage() {
selectedImageFile = null;
removeImageRequested = true;
currentImageUrl = null;
refreshImagePreview();
}
private void applyImageChanges(Long productId) throws Exception {
if (removeImageRequested) {
try {
ProductApi.getInstance().deleteProductImage(productId);
} catch (Exception ignored) {
}
}
if (selectedImageFile != null) {
ProductApi.getInstance().uploadProductImage(productId, selectedImageFile.toPath());
}
}
private void refreshImagePreview() {
if (imgProductPreview == null || lblImageStatus == null || btnRemoveImage == null) {
return;
}
imgProductPreview.setImage(null);
if (selectedImageFile != null) {
lblImageStatus.setText("Selected: " + selectedImageFile.getName());
DesktopImageSupport.loadImageInto(imgProductPreview, selectedImageFile.toURI().toString(), 120, 120);
btnRemoveImage.setDisable(false);
return;
}
if (currentImageUrl != null && !currentImageUrl.isBlank()) {
lblImageStatus.setText("Current image loaded");
DesktopImageSupport.loadImageInto(imgProductPreview, currentImageUrl, 120, 120);
btnRemoveImage.setDisable(false);
return;
}
lblImageStatus.setText("No image selected");
btnRemoveImage.setDisable(true);
}
} }

View File

@@ -12,8 +12,9 @@ public class Pet {
private SimpleIntegerProperty petAge; private SimpleIntegerProperty petAge;
private SimpleStringProperty petStatus; private SimpleStringProperty petStatus;
private SimpleDoubleProperty petPrice; private SimpleDoubleProperty petPrice;
private SimpleStringProperty imageUrl;
public Pet(int petId, String petName, String petSpecies, String petBreed, int petAge, String petStatus, double petPrice) { public Pet(int petId, String petName, String petSpecies, String petBreed, int petAge, String petStatus, double petPrice, String imageUrl) {
this.petId = new SimpleIntegerProperty(petId); this.petId = new SimpleIntegerProperty(petId);
this.petName = new SimpleStringProperty(petName); this.petName = new SimpleStringProperty(petName);
this.petSpecies = new SimpleStringProperty(petSpecies); this.petSpecies = new SimpleStringProperty(petSpecies);
@@ -21,6 +22,7 @@ public class Pet {
this.petAge = new SimpleIntegerProperty(petAge); this.petAge = new SimpleIntegerProperty(petAge);
this.petStatus = new SimpleStringProperty(petStatus); this.petStatus = new SimpleStringProperty(petStatus);
this.petPrice = new SimpleDoubleProperty(petPrice); this.petPrice = new SimpleDoubleProperty(petPrice);
this.imageUrl = new SimpleStringProperty(imageUrl);
} }
public int getPetId() { public int getPetId() {
@@ -106,4 +108,16 @@ public class Pet {
public SimpleDoubleProperty petPriceProperty() { public SimpleDoubleProperty petPriceProperty() {
return petPrice; return petPrice;
} }
public String getImageUrl() {
return imageUrl.get();
}
public void setImageUrl(String imageUrl) {
this.imageUrl.set(imageUrl);
}
public SimpleStringProperty imageUrlProperty() {
return imageUrl;
}
} }

View File

@@ -0,0 +1,53 @@
package org.example.petshopdesktop.util;
import javafx.application.Platform;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import org.example.petshopdesktop.api.ApiClient;
import java.io.ByteArrayInputStream;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class DesktopImageSupport {
private static final Map<String, Image> IMAGE_CACHE = new ConcurrentHashMap<>();
private DesktopImageSupport() {
}
public static void loadImageInto(ImageView imageView, String imageUrl, double width, double height) {
imageView.setFitWidth(width);
imageView.setFitHeight(height);
imageView.setPreserveRatio(true);
imageView.setImage(null);
if (imageUrl == null || imageUrl.isBlank()) {
return;
}
Image cached = IMAGE_CACHE.get(imageUrl);
if (cached != null) {
imageView.setImage(cached);
return;
}
new Thread(() -> {
try {
byte[] bytes = ApiClient.getInstance().getBytes(imageUrl);
Image image = new Image(new ByteArrayInputStream(bytes), width, height, true, true);
if (!image.isError()) {
IMAGE_CACHE.put(imageUrl, image);
Platform.runLater(() -> imageView.setImage(image));
}
} catch (Exception ignored) {
}
}, "desktop-image-loader").start();
}
public static void evict(String imageUrl) {
if (imageUrl != null && !imageUrl.isBlank()) {
IMAGE_CACHE.remove(imageUrl);
}
}
}

View File

@@ -5,6 +5,7 @@
<?import javafx.scene.control.ComboBox?> <?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?> <?import javafx.scene.control.TextField?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.ColumnConstraints?> <?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?> <?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
@@ -163,6 +164,22 @@
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" /> <Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
</VBox.margin> </VBox.margin>
</GridPane> </GridPane>
<HBox alignment="CENTER_LEFT" spacing="15.0">
<children>
<ImageView fx:id="imgPetPreview" fitHeight="120.0" fitWidth="120.0" pickOnBounds="true" preserveRatio="true" />
<VBox spacing="10.0">
<children>
<Label fx:id="lblImageStatus" text="No image selected" textFill="#2c3e50" />
<HBox spacing="10.0">
<children>
<Button fx:id="btnChangeImage" mnemonicParsing="false" text="Change Image" />
<Button fx:id="btnRemoveImage" mnemonicParsing="false" text="Remove Image" />
</children>
</HBox>
</children>
</VBox>
</children>
</HBox>
</children> </children>
<padding> <padding>
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" /> <Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />

View File

@@ -5,6 +5,7 @@
<?import javafx.scene.control.ComboBox?> <?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?> <?import javafx.scene.control.TextField?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.ColumnConstraints?> <?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?> <?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
@@ -137,6 +138,22 @@
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" /> <Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
</VBox.margin> </VBox.margin>
</GridPane> </GridPane>
<HBox alignment="CENTER_LEFT" spacing="15.0">
<children>
<ImageView fx:id="imgProductPreview" fitHeight="120.0" fitWidth="120.0" pickOnBounds="true" preserveRatio="true" />
<VBox spacing="10.0">
<children>
<Label fx:id="lblImageStatus" text="No image selected" textFill="#2c3e50" />
<HBox spacing="10.0">
<children>
<Button fx:id="btnChangeImage" mnemonicParsing="false" text="Change Image" />
<Button fx:id="btnRemoveImage" mnemonicParsing="false" text="Remove Image" />
</children>
</HBox>
</children>
</VBox>
</children>
</HBox>
</children> </children>
<padding> <padding>
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" /> <Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />

View File

@@ -6,6 +6,7 @@
<?import javafx.scene.control.TableColumn?> <?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?> <?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?> <?import javafx.scene.control.TextField?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?> <?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
@@ -67,13 +68,14 @@
</HBox> </HBox>
<TableView fx:id="tvPets" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS"> <TableView fx:id="tvPets" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
<columns> <columns>
<TableColumn fx:id="colPetId" prefWidth="60.0" text="ID" /> <TableColumn fx:id="colPetId" prefWidth="55.0" text="ID" />
<TableColumn fx:id="colPetName" prefWidth="113.14285278320312" text="Name" /> <TableColumn fx:id="colPetImage" prefWidth="80.0" text="Image" />
<TableColumn fx:id="colPetSpecies" prefWidth="110.28570556640625" text="Species" /> <TableColumn fx:id="colPetName" prefWidth="110.0" text="Name" />
<TableColumn fx:id="colPetBreed" prefWidth="174.85711669921875" text="Breed" /> <TableColumn fx:id="colPetSpecies" prefWidth="105.0" text="Species" />
<TableColumn fx:id="colPetAge" prefWidth="72.0" text="Age" /> <TableColumn fx:id="colPetBreed" prefWidth="145.0" text="Breed" />
<TableColumn fx:id="colPetStatus" prefWidth="133.142822265625" text="Status" /> <TableColumn fx:id="colPetAge" prefWidth="60.0" text="Age" />
<TableColumn fx:id="colPetPrice" prefWidth="89.142822265625" text="Price" /> <TableColumn fx:id="colPetStatus" prefWidth="110.0" text="Status" />
<TableColumn fx:id="colPetPrice" prefWidth="80.0" text="Price" />
</columns> </columns>
</TableView> </TableView>
</children> </children>

View File

@@ -67,11 +67,12 @@
</HBox> </HBox>
<TableView fx:id="tvProducts" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS"> <TableView fx:id="tvProducts" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
<columns> <columns>
<TableColumn fx:id="colProductId" prefWidth="60.0" text="ID" /> <TableColumn fx:id="colProductId" prefWidth="55.0" text="ID" />
<TableColumn fx:id="colProductName" prefWidth="170.85714721679688" text="Name" /> <TableColumn fx:id="colProductImage" prefWidth="80.0" text="Image" />
<TableColumn fx:id="colProductCategory" prefWidth="195.4285888671875" text="Category" /> <TableColumn fx:id="colProductName" prefWidth="150.0" text="Name" />
<TableColumn fx:id="colProductDesc" prefWidth="210.28570556640625" text="Description" /> <TableColumn fx:id="colProductCategory" prefWidth="160.0" text="Category" />
<TableColumn fx:id="colProductPrice" prefWidth="115.4285888671875" text="Price" /> <TableColumn fx:id="colProductDesc" prefWidth="195.0" text="Description" />
<TableColumn fx:id="colProductPrice" prefWidth="110.0" text="Price" />
</columns> </columns>
</TableView> </TableView>
</children> </children>