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 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/com/example/HelloModelTest.java b/src/test/java/com/example/HelloModelTest.java
new file mode 100644
index 00000000..36720844
--- /dev/null
+++ b/src/test/java/com/example/HelloModelTest.java
@@ -0,0 +1,32 @@
+package com.example;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class HelloModelTest {
+
+ @Test
+ void testModelInitializationWithEnv() {
+ try {
+ HelloModel model = new HelloModel();
+ assertNotNull(model, "Model should be created successfully with .env");
+ } catch (IllegalStateException e) {
+ assertTrue(e.getMessage().contains("BACKEND_URL"), "Exception should mention BACKEND_URL");
+ }
+ }
+
+ @Test
+ void testParseIncomingLineAddsEmoji() {
+ HelloModel model = new HelloModel("test-topic", "https://ntfy.sh");
+ String input = "{\"message\":\"Hej\"}";
+ String result = model.parseIncomingLine(input);
+ assertTrue(result.startsWith("💬 "), "Meddelandet ska börja med emoji");
+ assertTrue(result.contains("Hej"), "Meddelandet ska innehålla originaltexten");
+ }
+
+ @Test
+ void testSendMessageDoesNotThrow() {
+ HelloModel model = new HelloModel("test-topic", "https://ntfy.sh");
+ assertDoesNotThrow(() -> model.sendMessage("Testmeddelande"));
+ }
+}