Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f8d28e1
Första commit labb3. Skapat CSS-fil för att styra utseendet m.m.
MartinStenhagen Oct 31, 2025
d955f53
Uppdaterat MVC med serverhantering m.m.
MartinStenhagen Nov 4, 2025
3afa5ae
Uppdaterat programmet inför tester.
MartinStenhagen Nov 6, 2025
aa894db
Skapat tester som kör mot en fakeserver via Wiremock.
MartinStenhagen Nov 7, 2025
39ba6c0
Flyttat .env till rätt mapp i projektstrukturen och rättat kod i mode…
MartinStenhagen Nov 13, 2025
103a971
Anpassat koden efter att använda variabel message. timestamps i UI oc…
MartinStenhagen Nov 13, 2025
3ee6a4a
Lagt till funktion att skicka med bilder i det grafiska gränssnittet/…
MartinStenhagen Nov 13, 2025
01b14c1
buggfixar och uppdaterade tester
MartinStenhagen Nov 13, 2025
6f5015b
Förbättrad version som implementerar markdown
MartinStenhagen Nov 14, 2025
2f96c91
Version som fungerar inklusive tester.
MartinStenhagen Nov 14, 2025
fb7f611
Har ändrat topic till en DEFAULT_TOPIC-variabel efter AI-feedback.
MartinStenhagen Nov 14, 2025
c10d1f5
Uppdaterat handleSendImage() att fungera bättre på vissa plattformar.
MartinStenhagen Nov 14, 2025
3be7929
Uppdaterat uploadToLocalServer efter AI-feedback. HTTP-statuskontroll…
MartinStenhagen Nov 14, 2025
7b34374
Tydligare dokumentation i interfacet efter AI-feedback.
MartinStenhagen Nov 14, 2025
595194e
Förbättrad CSS-fil efter AI-feedback.
MartinStenhagen Nov 14, 2025
4e79512
Flyttat mvn till root efter AI-feedback.
MartinStenhagen Nov 14, 2025
f21c8af
Uppdaterat efter feedback.
MartinStenhagen Nov 14, 2025
9e6f3aa
Fixat felhantering i ImageServer.java
MartinStenhagen Nov 14, 2025
cc87a6a
Förbättringar i HelloController och HelloFX
MartinStenhagen Nov 14, 2025
2607d6f
Förbättringar i HelloController och HelloFX
MartinStenhagen Nov 14, 2025
08cce87
Förbättringar i HelloModel m.m
MartinStenhagen Nov 14, 2025
ba03b55
En förbättrad implementation efter AI-feedbacken.
MartinStenhagen Nov 14, 2025
6ade543
Tog bort en onödig dublett-import i ImageServer.java
MartinStenhagen Nov 14, 2025
df06cdd
Ändringar utifrån feedback från coderabbit.
MartinStenhagen Nov 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
target/
/.idea/
.env
9 changes: 9 additions & 0 deletions HelloFX.iml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/test" isTestSource="true" />
</content>
</component>
</module>
Empty file modified mvnw
100644 → 100755
Empty file.
68 changes: 55 additions & 13 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,30 @@
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>javafx</artifactId>
<artifactId>HelloFX</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.jupiter.version>6.0.0</junit.jupiter.version>
<junit.jupiter.version>5.10.0</junit.jupiter.version>
<assertj.core.version>3.27.6</assertj.core.version>
<mockito.version>5.20.0</mockito.version>
<javafx.version>25</javafx.version>
</properties>


<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand All @@ -36,16 +48,33 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>4.0.0-beta.15</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<artifactId>javafx-swing</artifactId>
<version>${javafx.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
Expand All @@ -54,15 +83,28 @@
<version>0.0.8</version>
<configuration>
<mainClass>com.example.HelloFX</mainClass>
<options>
<option>--enable-native-access=javafx.graphics</option>
</options>
<launcher>javafx</launcher>
<stripDebug>true</stripDebug>
<noHeaderFiles>true</noHeaderFiles>
<noManPages>true</noManPages>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>25</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<useModulePath>false</useModulePath>
<includes>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>

</project>
172 changes: 163 additions & 9 deletions src/main/java/com/example/HelloController.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,176 @@
package com.example;

import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.Window;

import java.io.File;
import java.time.Instant;
import java.time.ZoneId;

/**
* Controller layer: mediates between the view (FXML) and the model.
*/
public class HelloController {

private final HelloModel model = new HelloModel();
private HelloModel model;

@FXML private TextField messageField;
@FXML private VBox chatBox;
@FXML private ScrollPane chatScroll;
@FXML private Label statusLabel;

// Används av HelloFX för injection
public void setModel(HelloModel model) {
this.model = model;
attachListeners();
}

public void setConnection(NtfyConnection connection) {
if (connection == null) throw new IllegalArgumentException("Connection cannot be null");
this.model = new HelloModel(connection);
attachListeners();
}

private void attachListeners() {
model.getMessages().addListener((ListChangeListener<NtfyMessageDto>) change -> {
Platform.runLater(() -> {
while (change.next()) {
if (change.wasAdded()) {
for (NtfyMessageDto msg : change.getAddedSubList()) {
boolean sentByUser = msg.clientId() != null && msg.clientId().equals(model.getClientId());

if (msg.imageUrl() != null && !msg.imageUrl().isBlank()) {
addImageBubbleFromUrl(msg.imageUrl(), sentByUser, msg.time());
} else if (msg.message() != null && !msg.message().isBlank()) {
addMessageBubble(msg.message(), sentByUser, msg.time());
}
}
}
}
});
});
}

@FXML
private Label messageLabel;

@FXML
private void initialize() {
if (messageLabel != null) {
messageLabel.setText(model.getGreeting());
if (messageField != null) {
messageField.setOnAction(e -> handleSend());
}

chatBox.heightProperty().addListener((obs, oldVal, newVal) -> chatScroll.setVvalue(1.0));
}

@FXML
private void handleSend() {
if (model == null) return;
String message = messageField.getText().trim();
if (!message.isEmpty()) {
model.sendMessage(message);
messageField.clear();
model.setMessageToSend(""); // Clear property for bound text fields
}
}

@FXML
private void handleSendImage() {
if (model == null) return;

FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Välj en bild att skicka");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Bildfiler", "*.png", "*.jpg", "*.jpeg", "*.gif")
);

Window window = messageField.getScene() != null ? messageField.getScene().getWindow() : null;
File imageFile = fileChooser.showOpenDialog(window);

if (imageFile != null) {
if (!imageFile.exists()) {
showStatus("Fil finns inte: " + imageFile.getName());
return;
}

boolean success = model.sendImage(imageFile);
if (!success) {
showStatus("Misslyckades att skicka bilden: " + imageFile.getName());
}
}
}

private void addMessageBubble(String text, boolean isSentByUser, long timestamp) {
if (text == null || text.isBlank()) return;

Label messageLabel = new Label(text);
messageLabel.setWrapText(true);
messageLabel.setMaxWidth(400);
messageLabel.getStyleClass().add(isSentByUser ? "sent-message" : "received-message");

String timeString = Instant.ofEpochSecond(timestamp)
.atZone(ZoneId.systemDefault())
.toLocalTime().toString().substring(0, 5);
Label timeLabel = new Label(timeString);
timeLabel.getStyleClass().add("timestamp");

VBox bubble = new VBox(messageLabel, timeLabel);
bubble.setAlignment(isSentByUser ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT);

HBox container = new HBox(bubble);
container.setPadding(new Insets(5));
container.setAlignment(isSentByUser ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT);

chatBox.getChildren().add(container);
}

private void addImageBubbleFromUrl(String url, boolean isSentByUser, long timestamp) {
if (url == null || url.isBlank()) return;

ImageView imageView = new ImageView(new Image(url, true));
imageView.setFitWidth(200);
imageView.setPreserveRatio(true);
imageView.setSmooth(true);

imageView.setOnMouseClicked(e -> {
Stage stage = new Stage();
ImageView bigView = new ImageView(new Image(url));
bigView.setPreserveRatio(true);
bigView.setFitWidth(800);

ScrollPane sp = new ScrollPane(bigView);
sp.setFitToWidth(true);

stage.setScene(new Scene(sp, 900, 700));
stage.setTitle("Bildvisning");
stage.show();
});

String timeString = Instant.ofEpochSecond(timestamp)
.atZone(ZoneId.systemDefault())
.toLocalTime().toString().substring(0, 5);
Label timeLabel = new Label(timeString);
timeLabel.getStyleClass().add("timestamp");

VBox box = new VBox(imageView, timeLabel);
box.setSpacing(3);
box.setAlignment(isSentByUser ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT);

HBox wrapper = new HBox(box);
wrapper.setPadding(new Insets(5));
wrapper.setAlignment(isSentByUser ? Pos.CENTER_RIGHT : Pos.CENTER_LEFT);

chatBox.getChildren().add(wrapper);
}

public void showStatus(String text) {
Platform.runLater(() -> statusLabel.setText(text));
}
}
68 changes: 61 additions & 7 deletions src/main/java/com/example/HelloFX.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,78 @@

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import io.github.cdimascio.dotenv.Dotenv;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class HelloFX extends Application {

private NtfyConnection connection;
private ImageServer imageServer;

@Override
public void start(Stage stage) throws Exception {
public void start(Stage stage) {

Dotenv dotenv = Dotenv.load();
String hostName = dotenv.get("HOST_NAME");
if (hostName == null || hostName.isBlank()) {
showError("Configuration error", "HOST_NAME is not set in .env");
return;
}

connection = new NtfyConnectionImpl(hostName);

try {
Path imageDir = Path.of("images");
Files.createDirectories(imageDir);
imageServer = new ImageServer(8081, imageDir);
} catch (IOException e) {
showError("Image server error", "Could not start local image server:\n" + e.getMessage());
return;
}

FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml"));
Parent root = fxmlLoader.load();
Scene scene = new Scene(root, 640, 480);
stage.setTitle("Hello MVC");
Scene scene;
try {
scene = new Scene(fxmlLoader.load(), 600, 400);
} catch (IOException e) {
showError("FXML loading error", "Could not load GUI:\n" + e.getMessage());
return;
}

HelloController controller = fxmlLoader.getController();
controller.setConnection(connection);

stage.setTitle("HelloFX Chat");
stage.setScene(scene);
stage.show();

stage.setOnCloseRequest(event -> {
System.out.println("🛑 Application closing...");

try {
if (connection != null) connection.stopReceiving();
} catch (Exception e) {
e.printStackTrace();
}

try {
if (imageServer != null) imageServer.stop();
} catch (Exception e) {
e.printStackTrace();
}
});
}

private void showError(String title, String message) {
System.err.println("❌ " + title + ": " + message);
}

public static void main(String[] args) {
launch();
}

}
}
Loading
Loading