diff --git a/desktop/src/main/java/org/example/petshopdesktop/DTOs/ProductDTO.java b/desktop/src/main/java/org/example/petshopdesktop/DTOs/ProductDTO.java index 3ea081df..3270d6ca 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/DTOs/ProductDTO.java +++ b/desktop/src/main/java/org/example/petshopdesktop/DTOs/ProductDTO.java @@ -15,15 +15,17 @@ public class ProductDTO { private SimpleIntegerProperty categoryId; //used for edit and delete private SimpleStringProperty categoryName; private SimpleStringProperty prodDesc; + private SimpleStringProperty imageUrl; //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.prodName = new SimpleStringProperty(prodName); this.prodPrice = new SimpleDoubleProperty(prodPrice); this.categoryId = new SimpleIntegerProperty(categoryId); this.categoryName = new SimpleStringProperty(categoryName); this.prodDesc = new SimpleStringProperty(prodDesc); + this.imageUrl = new SimpleStringProperty(imageUrl); } //getter and setters @@ -99,6 +101,18 @@ public class ProductDTO { 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 * @return diff --git a/desktop/src/main/java/org/example/petshopdesktop/Validator.java b/desktop/src/main/java/org/example/petshopdesktop/Validator.java index cf51eb1e..2940cde4 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/Validator.java +++ b/desktop/src/main/java/org/example/petshopdesktop/Validator.java @@ -44,6 +44,33 @@ public class Validator { return msg; } + /** + * Checks if the input is a positive double + * @param value input of string + * @param name name of input + * @return error msg if input is not a number or not positive, otherwise empty + */ + public static String isPositiveDouble(String value, String name){ + String msg =""; + if (value == null) { + msg += name + " must be a number.\n"; + + return msg; + } + double result; + try { + result = Double.parseDouble(value); + if (result <= 0){ + msg += name + " must be greater than 0. \n"; + } + } + catch (NumberFormatException e){ + msg += name + " must be a number.\n"; + } + + return msg; + } + /** * Checks if the input is a double in 2 different range * @param value input of string @@ -95,6 +122,28 @@ public class Validator { return msg; } + /** + * Checks if the input is a positive integer + * @param value input of string + * @param name name of input + * @return error msg if input is not a number or not positive, otherwise empty + */ + public static String isPositiveInteger(String value, String name){ + String msg =""; + int result; + try { + result = Integer.parseInt(value); + if (result <= 0){ + msg += name + " must be greater than 0. \n"; + } + } + catch (NumberFormatException e){ + msg += name + " must be a whole number.\n"; + } + + return msg; + } + /** * check if the string is a given amount of characters or fewer * @param value input of string @@ -154,4 +203,4 @@ public class Validator { return msg; } -} \ No newline at end of file +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java b/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java index ed2e9fc6..3914d226 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java @@ -67,7 +67,7 @@ public class ApiClient { } else if (statusCode == 403) { throw new RuntimeException("Access restricted. You don't have permission to perform this action."); } else if (statusCode == 404) { - throw new RuntimeException("Avatar not found."); + throw new RuntimeException("File not found."); } else { throw new RuntimeException("Request failed with status " + statusCode); } @@ -224,15 +224,21 @@ public class ApiClient { try { if (response.body() != null && !response.body().isEmpty()) { var errorNode = objectMapper.readTree(response.body()); - if (errorNode.has("message")) { - return errorNode.get("message").asText(); - } if (errorNode.has("errors")) { StringBuilder sb = new StringBuilder(); errorNode.get("errors").fields().forEachRemaining(entry -> { - sb.append(entry.getValue().asText()).append("\n"); + String errorText = entry.getValue().asText(); + if (errorText != null && !errorText.isBlank()) { + sb.append(errorText).append("\n"); + } }); - return sb.toString().trim(); + if (sb.length() > 0) { + String message = errorNode.has("message") ? errorNode.get("message").asText() : null; + return (message != null && !message.isBlank() ? message + "\n" : "") + sb.toString().trim(); + } + } + if (errorNode.has("message")) { + return errorNode.get("message").asText(); } } } catch (Exception e) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java index b5fe23e9..f96372e8 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java @@ -9,6 +9,7 @@ import org.example.petshopdesktop.api.dto.pet.PetResponse; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.List; public class PetApi { @@ -47,6 +48,18 @@ public class PetApi { 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 ids) throws Exception { apiClient.deleteWithBody("/api/v1/pets", new BulkDeleteRequest(ids)); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java index 5bffd489..4b8c89f4 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java @@ -9,6 +9,7 @@ import org.example.petshopdesktop.api.dto.product.ProductResponse; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.List; public class ProductApi { @@ -47,6 +48,18 @@ public class ProductApi { 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 ids) throws Exception { apiClient.deleteWithBody("/api/v1/products", new BulkDeleteRequest(ids)); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java index 55ccb3e1..88928cb5 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java @@ -6,9 +6,12 @@ import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; import javafx.stage.Modality; import javafx.stage.Stage; 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.models.Pet; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.DesktopImageSupport; import java.io.IOException; import java.util.List; @@ -42,6 +46,9 @@ public class PetController { @FXML private TableColumn colPetId; + @FXML + private TableColumn colPetImage; + @FXML private TableColumn colPetName; @@ -134,12 +141,14 @@ public class PetController { tvPets.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); colPetId.setCellValueFactory(new PropertyValueFactory("petId")); + colPetImage.setCellValueFactory(new PropertyValueFactory("imageUrl")); colPetName.setCellValueFactory(new PropertyValueFactory("petName")); colPetSpecies.setCellValueFactory(new PropertyValueFactory("petSpecies")); colPetBreed.setCellValueFactory(new PropertyValueFactory("petBreed")); colPetAge.setCellValueFactory(new PropertyValueFactory("petAge")); colPetStatus.setCellValueFactory(new PropertyValueFactory("petStatus")); colPetPrice.setCellValueFactory(new PropertyValueFactory("petPrice")); + configureImageColumn(colPetImage); displayPets(); @@ -262,8 +271,30 @@ public class PetController { response.getPetBreed(), response.getPetAge() != null ? response.getPetAge() : 0, response.getPetStatus(), - response.getPetPrice().doubleValue() + response.getPetPrice().doubleValue(), + response.getImageUrl() ); } + private void configureImageColumn(TableColumn 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); + } + }); + } + } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java index e6168911..053e0105 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java @@ -6,9 +6,12 @@ import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; import javafx.stage.Modality; import javafx.stage.Stage; 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.controllers.dialogcontrollers.ProductDialogController; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.DesktopImageSupport; import java.io.IOException; import java.util.ArrayList; @@ -46,6 +50,9 @@ public class ProductController { @FXML private TableColumn colProductId; + @FXML + private TableColumn colProductImage; + @FXML private TableColumn colProductName; @@ -74,10 +81,12 @@ public class ProductController { tvProducts.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); //set up table columns colProductId.setCellValueFactory(new PropertyValueFactory("prodId")); + colProductImage.setCellValueFactory(new PropertyValueFactory("imageUrl")); colProductName.setCellValueFactory(new PropertyValueFactory("prodName")); colProductPrice.setCellValueFactory(new PropertyValueFactory("prodPrice")); colProductCategory.setCellValueFactory(new PropertyValueFactory("categoryName")); colProductDesc.setCellValueFactory(new PropertyValueFactory("prodDesc")); + configureImageColumn(colProductImage); displayProduct(); @@ -292,8 +301,30 @@ public class ProductController { response.getProdPrice().doubleValue(), 0, response.getCategoryName(), - response.getProdDesc() + response.getProdDesc(), + response.getImageUrl() ); } + private void configureImageColumn(TableColumn 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); + } + }); + } + } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java index cd7f2690..78ad836a 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java @@ -6,6 +6,7 @@ import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.*; +import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.stage.Stage; import org.example.petshopdesktop.Validator; @@ -14,8 +15,12 @@ import org.example.petshopdesktop.api.dto.pet.PetResponse; import org.example.petshopdesktop.api.endpoints.PetApi; import org.example.petshopdesktop.models.Pet; 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; + public class PetDialogController { @FXML @@ -24,6 +29,12 @@ public class PetDialogController { @FXML private Button btnSave; + @FXML + private Button btnChangeImage; + + @FXML + private Button btnRemoveImage; + @FXML private ComboBox cbPetStatus; @@ -33,6 +44,12 @@ public class PetDialogController { @FXML private Label lblPetId; + @FXML + private Label lblImageStatus; + + @FXML + private ImageView imgPetPreview; + @FXML private TextField txtPetAge; @@ -49,6 +66,9 @@ public class PetDialogController { private TextField txtPetSpecies; private String mode = null; + private File selectedImageFile; + private String currentImageUrl; + private boolean removeImageRequested; private ObservableList statusList = FXCollections.observableArrayList( "Available", "Adopted" @@ -73,6 +93,10 @@ public class PetDialogController { closeStage(mouseEvent); } }); + + btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage()); + btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage()); + refreshImagePreview(); } private void buttonSaveClicked(MouseEvent mouseEvent) { @@ -97,13 +121,14 @@ public class PetDialogController { //Check validation (format) errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price"); - errorMsg += Validator.isNonNegativeInteger(txtPetAge.getText(), "Age"); + errorMsg += Validator.isPositiveInteger(txtPetAge.getText(), "Age"); if(errorMsg.isEmpty()){ PetRequest request = buildPetRequest(); try { if(mode.equals("Add")) { - PetApi.getInstance().createPet(request); + PetResponse response = PetApi.getInstance().createPet(request); + applyImageChanges(response.getPetId()); } else { String[] parts = lblPetId.getText().split(": "); if (parts.length < 2) { @@ -111,6 +136,7 @@ public class PetDialogController { } Long petId = Long.parseLong(parts[1]); PetApi.getInstance().updatePet(petId, request); + applyImageChanges(petId); } //tell the user operation was successful @@ -175,6 +201,10 @@ public class PetDialogController { txtPetBreed.setText(pet.getPetBreed()); txtPetAge.setText(pet.getPetAge() + ""); txtPetPrice.setText(pet.getPetPrice() + ""); + currentImageUrl = pet.getImageUrl(); + selectedImageFile = null; + removeImageRequested = false; + refreshImagePreview(); //get the right combobox selection for (String status : cbPetStatus.getItems()) { @@ -192,10 +222,76 @@ public class PetDialogController { lblMode.setText(mode + " Pet"); if(mode.equals("Add")) { lblPetId.setVisible(false); + currentImageUrl = null; + selectedImageFile = null; + removeImageRequested = false; + refreshImagePreview(); } else if(mode.equals("Edit")) { 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 { + String previousImageUrl = currentImageUrl; + if (removeImageRequested) { + try { + PetApi.getInstance().deletePetImage(petId); + } catch (Exception ignored) { + } + } + if (selectedImageFile != null) { + PetApi.getInstance().uploadPetImage(petId, selectedImageFile.toPath()); + currentImageUrl = "/api/v1/pets/" + petId + "/image"; + } else if (removeImageRequested) { + currentImageUrl = null; + } + DesktopImageSupport.evict(previousImageUrl); + DesktopImageSupport.evict(currentImageUrl); + selectedImageFile = null; + removeImageRequested = false; + refreshImagePreview(); + } + + 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); + } + } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductDialogController.java index 25fe1da8..7b354a9d 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductDialogController.java @@ -6,16 +6,21 @@ import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.*; +import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.stage.Stage; import org.example.petshopdesktop.DTOs.ProductDTO; import org.example.petshopdesktop.Validator; import org.example.petshopdesktop.api.dto.common.DropdownOption; 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.ProductApi; 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.util.List; @@ -27,6 +32,12 @@ public class ProductDialogController { @FXML private Button btnSave; + @FXML + private Button btnChangeImage; + + @FXML + private Button btnRemoveImage; + @FXML private ComboBox cbProdCategory; @@ -36,6 +47,12 @@ public class ProductDialogController { @FXML private Label lblProdId; + @FXML + private Label lblImageStatus; + + @FXML + private ImageView imgProductPreview; + @FXML private TextField txtProdDesc; @@ -46,6 +63,9 @@ public class ProductDialogController { private TextField txtProdPrice; private String mode = null; + private File selectedImageFile; + private String currentImageUrl; + private boolean removeImageRequested; /** * Add event listeners to buttons when dialog loads @@ -82,6 +102,10 @@ public class ProductDialogController { System.out.println("Error loading categories: " + e.getMessage()); } + btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage()); + btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage()); + refreshImagePreview(); + } /** @@ -106,7 +130,7 @@ public class ProductDialogController { errorMsg += Validator.isLessThanVarChars(txtProdPrice.getText(), "Product Price", 12); //Check Validation (format) - errorMsg += Validator.isNonNegativeDouble(txtProdPrice.getText(), "Product Price"); + errorMsg += Validator.isPositiveDouble(txtProdPrice.getText(), "Product Price"); if (errorMsg.isEmpty()) { try { @@ -123,7 +147,8 @@ public class ProductDialogController { request.setProdDesc(txtProdDesc.getText()); if (mode.equals("Add")) { - ProductApi.getInstance().createProduct(request); + ProductResponse response = ProductApi.getInstance().createProduct(request); + applyImageChanges(response.getProdId()); } else { String[] parts = lblProdId.getText().split(": "); if (parts.length < 2) { @@ -131,6 +156,7 @@ public class ProductDialogController { } Long productId = Long.parseLong(parts[1]); ProductApi.getInstance().updateProduct(productId, request); + applyImageChanges(productId); } Alert alert = new Alert(Alert.AlertType.INFORMATION); @@ -167,6 +193,10 @@ public class ProductDialogController { txtProdName.setText(product.getProdName()); txtProdDesc.setText(product.getProdDesc()); txtProdPrice.setText(product.getProdPrice() + ""); + currentImageUrl = product.getImageUrl(); + selectedImageFile = null; + removeImageRequested = false; + refreshImagePreview(); for (DropdownOption category : cbProdCategory.getItems()) { if(category.getLabel().equals(product.getCategoryName())){ @@ -197,10 +227,76 @@ public class ProductDialogController { lblMode.setText(mode + " Product"); if(mode.equals("Add")) { lblProdId.setVisible(false); + currentImageUrl = null; + selectedImageFile = null; + removeImageRequested = false; + refreshImagePreview(); } else if(mode.equals("Edit")) { 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 { + String previousImageUrl = currentImageUrl; + if (removeImageRequested) { + try { + ProductApi.getInstance().deleteProductImage(productId); + } catch (Exception ignored) { + } + } + if (selectedImageFile != null) { + ProductApi.getInstance().uploadProductImage(productId, selectedImageFile.toPath()); + currentImageUrl = "/api/v1/products/" + productId + "/image"; + } else if (removeImageRequested) { + currentImageUrl = null; + } + DesktopImageSupport.evict(previousImageUrl); + DesktopImageSupport.evict(currentImageUrl); + selectedImageFile = null; + removeImageRequested = false; + refreshImagePreview(); + } + + 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); + } + } diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java b/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java index e1f2e3fb..fc1723c5 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java @@ -12,8 +12,9 @@ public class Pet { private SimpleIntegerProperty petAge; private SimpleStringProperty petStatus; 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.petName = new SimpleStringProperty(petName); this.petSpecies = new SimpleStringProperty(petSpecies); @@ -21,6 +22,7 @@ public class Pet { this.petAge = new SimpleIntegerProperty(petAge); this.petStatus = new SimpleStringProperty(petStatus); this.petPrice = new SimpleDoubleProperty(petPrice); + this.imageUrl = new SimpleStringProperty(imageUrl); } public int getPetId() { @@ -106,4 +108,16 @@ public class Pet { public SimpleDoubleProperty petPriceProperty() { return petPrice; } + + public String getImageUrl() { + return imageUrl.get(); + } + + public void setImageUrl(String imageUrl) { + this.imageUrl.set(imageUrl); + } + + public SimpleStringProperty imageUrlProperty() { + return imageUrl; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/util/DesktopImageSupport.java b/desktop/src/main/java/org/example/petshopdesktop/util/DesktopImageSupport.java new file mode 100644 index 00000000..59cbcb98 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/util/DesktopImageSupport.java @@ -0,0 +1,62 @@ +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 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.setSmooth(true); + imageView.setImage(null); + + if (imageUrl == null || imageUrl.isBlank()) { + return; + } + + if (imageUrl.startsWith("file:")) { + Image image = new Image(imageUrl, 0, 0, true, true); + if (!image.isError()) { + imageView.setImage(image); + } + 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)); + 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); + } + } +} diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml index d4c97ddb..2f5bd110 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml @@ -5,15 +5,15 @@ + - - + @@ -62,18 +62,13 @@ - + - + - - - - - @@ -163,6 +158,22 @@ + + + + + +