diff --git a/.gitignore b/.gitignore index 6ac465db..244268f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ /.idea/ +.env diff --git a/mvnw b/mvnw old mode 100644 new mode 100755 diff --git a/pom.xml b/pom.xml index c40f667e..b8c4e603 100644 --- a/pom.xml +++ b/pom.xml @@ -4,65 +4,70 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - com.example - javafx + org.example + JavaFXChatApp 1.0-SNAPSHOT 25 UTF-8 - 6.0.0 - 3.27.6 - 5.20.0 - 25 + 5.13.4 + + - org.junit.jupiter - junit-jupiter - ${junit.jupiter.version} - test + org.openjfx + javafx-controls + 25 - org.assertj - assertj-core - ${assertj.core.version} - test + org.openjfx + javafx-fxml + 25 + + - org.mockito - mockito-junit-jupiter - ${mockito.version} - test + io.github.cdimascio + dotenv-java + 3.2.0 + + - org.openjfx - javafx-controls - ${javafx.version} + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + - org.openjfx - javafx-fxml - ${javafx.version} + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + org.openjfx javafx-maven-plugin 0.0.8 com.example.HelloFX - - - - javafx - true - true - true + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java index fdd160a0..8d995951 100644 --- a/src/main/java/com/example/HelloController.java +++ b/src/main/java/com/example/HelloController.java @@ -1,22 +1,58 @@ package com.example; +import javafx.application.Platform; import javafx.fxml.FXML; -import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.stage.FileChooser; +import javafx.scene.control.Button; + +import java.io.File; -/** - * Controller layer: mediates between the view (FXML) and the model. - */ public class HelloController { - private final HelloModel model = new HelloModel(); + @FXML + private TextArea chatArea; + + @FXML + private TextField inputField; + + @FXML + private Button sendButton; @FXML - private Label messageLabel; + private Button attachButton; + + private HelloModel model; + + @FXML + public void initialize() { + // Läser BACKEND_URL och TOPIC från .env via HelloModel + model = new HelloModel(); + + // Lyssna på inkommande meddelanden + model.listen(msg -> { + Platform.runLater(() -> chatArea.appendText(msg + "\n")); + System.out.println("📩 Mottaget: " + msg); + }); + } + + @FXML + protected void onSendButtonClick() { + String message = inputField.getText().trim(); + if (!message.isEmpty()) { + model.sendMessage(message); + inputField.clear(); + } + } @FXML - private void initialize() { - if (messageLabel != null) { - messageLabel.setText(model.getGreeting()); + protected void onAttachFileClick() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Välj en fil att skicka"); + File file = fileChooser.showOpenDialog(chatArea.getScene().getWindow()); + if (file != null) { + model.sendFile(file); } } } diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index 96bdc5ca..10edc2cb 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -10,10 +10,18 @@ public class HelloFX extends Application { @Override public void start(Stage stage) throws Exception { + // Ladda FXML FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml")); Parent root = fxmlLoader.load(); + + // Skapa scenen Scene scene = new Scene(root, 640, 480); - stage.setTitle("Hello MVC"); + + // Koppla in CSS-styling + scene.getStylesheets().add(HelloFX.class.getResource("style.css").toExternalForm()); + + // Sätt titel + stage.setTitle("Java Chat"); stage.setScene(scene); stage.show(); } @@ -21,5 +29,4 @@ public void start(Stage stage) throws Exception { public static void main(String[] args) { launch(); } - -} \ No newline at end of file +} diff --git a/src/main/java/com/example/HelloModel.java b/src/main/java/com/example/HelloModel.java index 385cfd10..240f413d 100644 --- a/src/main/java/com/example/HelloModel.java +++ b/src/main/java/com/example/HelloModel.java @@ -1,15 +1,140 @@ package com.example; -/** - * Model layer: encapsulates application data and business logic. - */ +import io.github.cdimascio.dotenv.Dotenv; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.File; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.util.concurrent.CompletableFuture; + public class HelloModel { - /** - * Returns a greeting based on the current Java and JavaFX versions. - */ - public String getGreeting() { - String javaVersion = System.getProperty("java.version"); - String javafxVersion = System.getProperty("javafx.version"); - return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + "."; + + private static final ObjectMapper mapper = new ObjectMapper(); + + private final HttpClient client = HttpClient.newHttpClient(); + private final String topic; + private final String backendUrl; + + /** Standardkonstruktor som läser från .env */ + public HelloModel() { + Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load(); + this.backendUrl = dotenv.get("BACKEND_URL", System.getenv("BACKEND_URL")); + this.topic = dotenv.get("TOPIC", System.getenv("TOPIC")); + if (backendUrl == null || topic == null) { + throw new IllegalStateException("BACKEND_URL eller TOPIC saknas i .env"); + } + } + + /** Alternativ konstruktor för tester */ + HelloModel(String topic, String backendUrl) { + if (backendUrl == null || backendUrl.isBlank()) { + throw new IllegalArgumentException("backendUrl must not be null/blank"); + } + this.backendUrl = backendUrl; + this.topic = topic; + } + + public void sendMessage(String message) { + String sender = "[Eric Chat App]"; + String fullMessage = sender + " " + message; + + String json = "{\"message\": \"" + fullMessage.replace("\"", "\\\"") + "\"}"; + String url = backendUrl + "/" + topic; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenAccept(response -> { + if (response.statusCode() >= 300) { + System.err.println("⚠️ Misslyckades att skicka: " + response.statusCode() + " - " + response.body()); + } + }) + .exceptionally(ex -> { + System.err.println("⚠️ Nätverksfel vid sendMessage: " + ex.getMessage()); + return null; + }); + } + + public void sendFile(File file) { + try { + String url = backendUrl + "/" + topic; + String contentType = Files.probeContentType(file.toPath()); + if (contentType == null) contentType = "application/octet-stream"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", contentType) + .header("X-Filename", file.getName()) + .header("Title", "File: " + file.getName()) + .POST(HttpRequest.BodyPublishers.ofFile(file.toPath())) + .build(); + + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenAccept(response -> { + if (response.statusCode() >= 300) { + System.err.println("⚠️ Filupload misslyckades: " + response.statusCode() + " - " + response.body()); + } else { + System.out.println("✅ Fil skickad: " + file.getName()); + } + }) + .exceptionally(ex -> { + System.err.println("⚠️ Nätverksfel vid sendFile: " + ex.getMessage()); + return null; + }); + } catch (Exception e) { + System.err.println("⚠️ Kunde inte läsa/skicka fil: " + e.getMessage()); + } + } + + public CompletableFuture listen(MessageHandler handler) { + String url = backendUrl + "/" + topic + "/json"; + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build(); + + return client.sendAsync(request, HttpResponse.BodyHandlers.ofLines()) + .thenAccept(response -> response.body().forEach(line -> { + String parsed = parseIncomingLine(line); + if (!parsed.isEmpty()) { + handler.onMessage(parsed); + System.out.println("📩 Meddelande: " + parsed); + } + })) + .exceptionally(ex -> { + System.err.println("⚠️ Nätverksfel vid listen: " + ex.getMessage()); + return null; + }); + } + + String parseIncomingLine(String line) { + try { + JsonNode outer = mapper.readTree(line); + String raw = outer.path("message").asText(""); + if (raw.isEmpty()) return ""; + + String clean = raw.startsWith("{") + ? mapper.readTree(raw).path("message").asText(raw) + : raw; + + if (!clean.contains("[Eric Chat App]") && !clean.contains("[Javafx-chat]")) { + clean = "[Javafx-chat] " + clean; + } + + return "💬 " + clean; + } catch (Exception e) { + System.err.println("⚠️ Kunde inte tolka rad: " + line + " | " + e.getMessage()); + return ""; + } + } + + public interface MessageHandler { + void onMessage(String message); } } diff --git a/src/main/java/com/example/NtfyMessageDto.java b/src/main/java/com/example/NtfyMessageDto.java new file mode 100644 index 00000000..7e865b17 --- /dev/null +++ b/src/main/java/com/example/NtfyMessageDto.java @@ -0,0 +1,6 @@ +package com.example; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record NtfyMessageDto(String event, String topic, String message) {} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 71574a27..f2f67a0a 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,7 +1,11 @@ module hellofx { requires javafx.controls; requires javafx.fxml; + requires java.net.http; + requires io.github.cdimascio.dotenv.java; + requires com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.annotation; - opens com.example to javafx.fxml; + opens com.example to javafx.fxml, com.fasterxml.jackson.databind; exports com.example; -} \ No newline at end of file +} diff --git a/src/main/resources/com/example/Style.css b/src/main/resources/com/example/Style.css new file mode 100644 index 00000000..781efea9 --- /dev/null +++ b/src/main/resources/com/example/Style.css @@ -0,0 +1,80 @@ +/* Global root styling */ +.root { + -fx-background-color: linear-gradient(to bottom, #e6f2ff, #cce0ff); /* mjuk ljusblå gradient */ + -fx-border-color: #ffffff; + -fx-border-width: 4; + -fx-border-radius: 12; + -fx-background-radius: 12; + -fx-font-family: "Segoe UI Emoji", "Segoe UI", sans-serif; + -fx-font-size: 14px; +} + +/* Titel högst upp */ +.app-title { + -fx-text-fill: #003366; /* mörkblå text */ + -fx-font-size: 28px; + -fx-font-weight: bold; + -fx-padding: 16; + -fx-alignment: center; + -fx-background-color: #ffffff; + -fx-background-radius: 0 0 12 12; + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 6,0,0,2); +} + +/* Chat-området */ +.chat-area { + -fx-control-inner-background: #ffffff; + -fx-text-fill: #000000; + -fx-border-color: #99ccff; + -fx-border-radius: 8; + -fx-background-radius: 8; + -fx-padding: 12; + -fx-font-size: 14px; + -fx-effect: innershadow(gaussian, rgba(0,0,0,0.1), 4,0,0,2); +} + +/* Inputfält */ +.chat-input { + -fx-background-color: #ffffff; + -fx-text-fill: #000000; + -fx-border-color: #99ccff; + -fx-border-radius: 8; + -fx-background-radius: 8; + -fx-padding: 8; + -fx-font-size: 14px; +} + +/* Input-bar längst ner */ +.input-bar { + -fx-background-color: #f0f8ff; + -fx-background-radius: 8; + -fx-padding: 8; +} + +/* Skicka-knappen */ +.send-btn { + -fx-background-color: #4a90e2; + -fx-text-fill: #ffffff; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-padding: 10 20; + -fx-cursor: hand; + -fx-font-size: 14px; +} +.send-btn:hover { + -fx-background-color: #003366; /* mörkblå vid hover */ +} + +/* Attach File-knappen */ +.attach-btn { + -fx-background-color: #50c9c3; + -fx-text-fill: #ffffff; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-padding: 10 20; + -fx-cursor: hand; + -fx-font-size: 14px; +} +.attach-btn:hover { + -fx-background-color: #3aa7a0; /* mörkare turkos vid hover */ +} diff --git a/src/main/resources/com/example/hello-view.fxml b/src/main/resources/com/example/hello-view.fxml index 20a7dc82..db96804b 100644 --- a/src/main/resources/com/example/hello-view.fxml +++ b/src/main/resources/com/example/hello-view.fxml @@ -1,9 +1,37 @@ - - - - - - - + + + + + + + + + + + +
+