diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index c56bbb22..3e89b391 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -3,6 +3,7 @@ module org.example.petshopdesktop { requires javafx.fxml; requires java.sql; requires java.net.http; + requires java.xml; requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.core; requires com.fasterxml.jackson.annotation; @@ -14,6 +15,7 @@ module org.example.petshopdesktop { opens org.example.petshopdesktop.controllers.dialogcontrollers to javafx.fxml; opens org.example.petshopdesktop.controllers to javafx.fxml; opens org.example.petshopdesktop.auth to javafx.fxml; + opens org.example.petshopdesktop.ui to javafx.fxml; opens org.example.petshopdesktop.api.dto.common to com.fasterxml.jackson.databind; opens org.example.petshopdesktop.api.dto.auth to com.fasterxml.jackson.databind; diff --git a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java index 96dece06..f85c6e76 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java @@ -8,7 +8,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; -import javafx.stage.Modality; +import javafx.scene.layout.StackPane; import javafx.stage.Stage; import org.example.petshopdesktop.api.ApiClient; import org.example.petshopdesktop.api.dto.auth.LoginRequest; @@ -16,6 +16,7 @@ import org.example.petshopdesktop.api.dto.auth.LoginResponse; import org.example.petshopdesktop.api.dto.auth.UserInfoResponse; import org.example.petshopdesktop.auth.Role; import org.example.petshopdesktop.auth.UserSession; +import org.example.petshopdesktop.ui.SvgNodeLoader; import org.example.petshopdesktop.util.ActivityLogger; public class LoginController { @@ -29,9 +30,13 @@ public class LoginController { @FXML private Label lblError; + @FXML + private StackPane logoContainer; + @FXML public void initialize() { lblError.setText(""); + logoContainer.getChildren().setAll(SvgNodeLoader.loadSquare("/org/example/petshopdesktop/images/leons-pet-store-badge-text.svg", 190)); } @FXML diff --git a/src/main/java/org/example/petshopdesktop/ui/SvgNodeLoader.java b/src/main/java/org/example/petshopdesktop/ui/SvgNodeLoader.java new file mode 100644 index 00000000..f4578114 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/ui/SvgNodeLoader.java @@ -0,0 +1,196 @@ +package org.example.petshopdesktop.ui; + +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Paint; +import javafx.scene.shape.Circle; +import javafx.scene.shape.FillRule; +import javafx.scene.shape.Shape; +import javafx.scene.shape.SVGPath; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class SvgNodeLoader { + private SvgNodeLoader() { + } + + public static StackPane loadSquare(String resourcePath, double size) { + SvgData svgData = readSvg(resourcePath); + Group translated = new Group(svgData.content()); + translated.setTranslateX(-svgData.minX()); + translated.setTranslateY(-svgData.minY()); + + double scale = Math.min(size / svgData.width(), size / svgData.height()); + translated.setScaleX(scale); + translated.setScaleY(scale); + + StackPane pane = new StackPane(translated); + pane.setMinSize(size, size); + pane.setPrefSize(size, size); + pane.setMaxSize(size, size); + return pane; + } + + private static SvgData readSvg(String resourcePath) { + try (Reader reader = requireResource(resourcePath)) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(false); + Document document = factory.newDocumentBuilder().parse(new InputSource(reader)); + Element root = document.getDocumentElement(); + double[] viewBox = parseViewBox(root.getAttribute("viewBox")); + Map> styles = parseStyles(root); + Group content = new Group(); + appendChildren(root, content, styles); + return new SvgData(content, viewBox[0], viewBox[1], viewBox[2], viewBox[3]); + } catch (Exception e) { + throw new IllegalStateException("Unable to load SVG: " + resourcePath, e); + } + } + + private static InputStreamReader requireResource(String resourcePath) { + var stream = SvgNodeLoader.class.getResourceAsStream(resourcePath); + if (stream == null) { + throw new IllegalArgumentException("Resource not found: " + resourcePath); + } + return new InputStreamReader(stream, StandardCharsets.UTF_8); + } + + private static double[] parseViewBox(String viewBox) { + String[] parts = viewBox.trim().split("\\s+"); + if (parts.length != 4) { + throw new IllegalArgumentException("Invalid viewBox: " + viewBox); + } + return new double[]{ + Double.parseDouble(parts[0]), + Double.parseDouble(parts[1]), + Double.parseDouble(parts[2]), + Double.parseDouble(parts[3]) + }; + } + + private static Map> parseStyles(Element root) { + Map> styles = new HashMap<>(); + NodeList styleNodes = root.getElementsByTagName("style"); + for (int i = 0; i < styleNodes.getLength(); i++) { + String css = styleNodes.item(i).getTextContent(); + for (String rule : css.split("}")) { + String trimmedRule = rule.trim(); + if (trimmedRule.isEmpty()) { + continue; + } + String[] selectorAndBody = trimmedRule.split("\\{", 2); + if (selectorAndBody.length != 2) { + continue; + } + String selector = selectorAndBody[0].trim(); + if (!selector.startsWith(".")) { + continue; + } + Map properties = new LinkedHashMap<>(); + for (String declaration : selectorAndBody[1].split(";")) { + String trimmedDeclaration = declaration.trim(); + if (trimmedDeclaration.isEmpty()) { + continue; + } + String[] keyValue = trimmedDeclaration.split(":", 2); + if (keyValue.length == 2) { + properties.put(keyValue[0].trim(), keyValue[1].trim()); + } + } + styles.put(selector.substring(1), properties); + } + } + return styles; + } + + private static void appendChildren(Element parent, Group target, Map> styles) { + NodeList childNodes = parent.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + org.w3c.dom.Node child = childNodes.item(i); + if (!(child instanceof Element element)) { + continue; + } + switch (element.getTagName()) { + case "g" -> { + Group group = new Group(); + appendChildren(element, group, styles); + target.getChildren().add(group); + } + case "circle" -> target.getChildren().add(buildCircle(element, styles)); + case "path" -> target.getChildren().add(buildPath(element, styles)); + default -> { + } + } + } + } + + private static Circle buildCircle(Element element, Map> styles) { + Circle circle = new Circle( + Double.parseDouble(element.getAttribute("cx")), + Double.parseDouble(element.getAttribute("cy")), + Double.parseDouble(element.getAttribute("r")) + ); + applyShapeStyle(circle, element, styles); + return circle; + } + + private static SVGPath buildPath(Element element, Map> styles) { + SVGPath path = new SVGPath(); + path.setContent(element.getAttribute("d")); + applyShapeStyle(path, element, styles); + return path; + } + + private static void applyShapeStyle(Shape shape, Element element, Map> styles) { + Map styleMap = new LinkedHashMap<>(); + String className = element.getAttribute("class"); + if (!className.isBlank()) { + for (String cssClass : className.trim().split("\\s+")) { + Map classStyles = styles.get(cssClass); + if (classStyles != null) { + styleMap.putAll(classStyles); + } + } + } + copyAttribute(element, styleMap, "fill"); + copyAttribute(element, styleMap, "stroke"); + copyAttribute(element, styleMap, "stroke-width"); + copyAttribute(element, styleMap, "fill-rule"); + + if (styleMap.containsKey("fill") && !"none".equalsIgnoreCase(styleMap.get("fill"))) { + shape.setFill(Paint.valueOf(styleMap.get("fill"))); + } else { + shape.setFill(null); + } + if (styleMap.containsKey("stroke") && !"none".equalsIgnoreCase(styleMap.get("stroke"))) { + shape.setStroke(Paint.valueOf(styleMap.get("stroke"))); + } + if (styleMap.containsKey("stroke-width")) { + shape.setStrokeWidth(Double.parseDouble(styleMap.get("stroke-width"))); + } + if (styleMap.containsKey("fill-rule") && shape instanceof SVGPath svgPath) { + svgPath.setFillRule("evenodd".equalsIgnoreCase(styleMap.get("fill-rule")) ? FillRule.EVEN_ODD : FillRule.NON_ZERO); + } + } + + private static void copyAttribute(Element element, Map styleMap, String attribute) { + String value = element.getAttribute(attribute); + if (!value.isBlank()) { + styleMap.put(attribute, value); + } + } + + private record SvgData(Group content, double minX, double minY, double width, double height) { + } +} diff --git a/src/main/resources/org/example/petshopdesktop/login-view.fxml b/src/main/resources/org/example/petshopdesktop/login-view.fxml index b74ffaf6..5d40ce50 100644 --- a/src/main/resources/org/example/petshopdesktop/login-view.fxml +++ b/src/main/resources/org/example/petshopdesktop/login-view.fxml @@ -5,9 +5,10 @@ - - + + + - - - - + + + + - +