From 3e14db491262ca341672fb3593bb4949ad5c5824 Mon Sep 17 00:00:00 2001 From: synopss Date: Sat, 10 May 2025 01:48:28 +0200 Subject: [PATCH 1/3] feat: adding atlantafx for a more modern UI --- build.gradle | 4 ++++ .../java/com/synops/replayreader/core/StageInitializer.java | 6 ++++++ src/main/java/com/synops/replayreader/ui/util/UiUtil.java | 4 ++++ src/main/resources/css/theme.css | 0 4 files changed, 14 insertions(+) create mode 100644 src/main/resources/css/theme.css diff --git a/build.gradle b/build.gradle index 34f2178..2ea1b22 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,10 @@ dependencies { implementation 'com.google.code.gson:gson:2.13.1' implementation 'org.apache.commons:commons-lang3:3.17.0' implementation 'com.google.guava:guava:33.4.8-jre' + implementation 'io.github.mkpaz:atlantafx-base:2.0.1' + implementation platform('org.kordamp.ikonli:ikonli-bom:12.4.0') + implementation 'org.kordamp.ikonli:ikonli-fluentui-pack:12.4.0' + implementation 'org.kordamp.ikonli:ikonli-javafx:12.4.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/java/com/synops/replayreader/core/StageInitializer.java b/src/main/java/com/synops/replayreader/core/StageInitializer.java index 0ccb5dd..9308ec4 100644 --- a/src/main/java/com/synops/replayreader/core/StageInitializer.java +++ b/src/main/java/com/synops/replayreader/core/StageInitializer.java @@ -1,9 +1,12 @@ package com.synops.replayreader.core; +import atlantafx.base.theme.PrimerDark; +import atlantafx.base.theme.PrimerLight; import com.synops.replayreader.common.i18n.I18nUtils; import com.synops.replayreader.ui.util.UiUtil; import java.io.IOException; import java.util.ResourceBundle; +import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; @@ -41,10 +44,13 @@ public void onApplicationEvent(StageReadyEvent event) { fxmlLoader.setControllerFactory(applicationContext::getBean); Parent parent = fxmlLoader.load(); var stage = event.getStage(); +// Application.setUserAgentStylesheet(new PrimerDark().getUserAgentStylesheet()); + Application.setUserAgentStylesheet(new PrimerLight().getUserAgentStylesheet()); stage.setScene(new Scene(parent, applicationWidth, applicationHeight)); stage.setResizable(false); stage.setTitle(resourceBundle.getString("main.title")); UiUtil.setDefaultIcon(stage); + UiUtil.setDefaultStyle(stage); stage.show(); } catch (IOException e) { throw new RuntimeException(e); diff --git a/src/main/java/com/synops/replayreader/ui/util/UiUtil.java b/src/main/java/com/synops/replayreader/ui/util/UiUtil.java index dec2eb9..a51c57d 100644 --- a/src/main/java/com/synops/replayreader/ui/util/UiUtil.java +++ b/src/main/java/com/synops/replayreader/ui/util/UiUtil.java @@ -10,4 +10,8 @@ public static void setDefaultIcon(Stage stage) { stage.getIcons().add( new Image(Objects.requireNonNull(stage.getClass().getResourceAsStream("/image/icon.png")))); } + + public static void setDefaultStyle(Stage stage) { + stage.getScene().getStylesheets().add("/css/theme.css"); + } } diff --git a/src/main/resources/css/theme.css b/src/main/resources/css/theme.css new file mode 100644 index 0000000..e69de29 From 1cb8cdea99ede1c61012fcfdf858e4be00ce2beb Mon Sep 17 00:00:00 2001 From: synopss Date: Sat, 10 May 2025 01:51:42 +0200 Subject: [PATCH 2/3] feat: adding NotificationService --- .../ReplayReaderConfiguration.java | 11 ++- .../core/service/NotificationService.java | 17 +++++ .../core/service/NotificationServiceImpl.java | 76 +++++++++++++++++++ .../ui/controller/MainViewController.java | 14 ++-- src/main/resources/views/main.fxml | 10 +-- 5 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/synops/replayreader/core/service/NotificationService.java create mode 100644 src/main/java/com/synops/replayreader/core/service/NotificationServiceImpl.java diff --git a/src/main/java/com/synops/replayreader/core/configuration/ReplayReaderConfiguration.java b/src/main/java/com/synops/replayreader/core/configuration/ReplayReaderConfiguration.java index 2c08019..416cc49 100644 --- a/src/main/java/com/synops/replayreader/core/configuration/ReplayReaderConfiguration.java +++ b/src/main/java/com/synops/replayreader/core/configuration/ReplayReaderConfiguration.java @@ -1,16 +1,18 @@ package com.synops.replayreader.core.configuration; import com.synops.replayreader.clan.comparator.ClanListComparator; +import com.synops.replayreader.clan.util.ClanStringConverter; +import com.synops.replayreader.common.comparator.SortingComparators; import com.synops.replayreader.core.service.DialogService; import com.synops.replayreader.core.service.DialogServiceImpl; +import com.synops.replayreader.core.service.NotificationService; +import com.synops.replayreader.core.service.NotificationServiceImpl; import com.synops.replayreader.maps.comparator.MapsComparator; -import com.synops.replayreader.common.comparator.SortingComparators; import com.synops.replayreader.replay.controller.ReplayController; import com.synops.replayreader.replay.service.ReplayModelBuilder; import com.synops.replayreader.replay.service.ReplayReader; import com.synops.replayreader.replay.service.ReplayService; import com.synops.replayreader.replay.service.ReplayServiceImpl; -import com.synops.replayreader.clan.util.ClanStringConverter; import com.synops.replayreader.ui.util.DragDropSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -28,6 +30,11 @@ public DialogService dialogService() { return new DialogServiceImpl(); } + @Bean + public NotificationService notificationService() { + return new NotificationServiceImpl(); + } + @Bean public ClanStringConverter clanStringConverter() { return new ClanStringConverter(); diff --git a/src/main/java/com/synops/replayreader/core/service/NotificationService.java b/src/main/java/com/synops/replayreader/core/service/NotificationService.java new file mode 100644 index 0000000..423ae30 --- /dev/null +++ b/src/main/java/com/synops/replayreader/core/service/NotificationService.java @@ -0,0 +1,17 @@ +package com.synops.replayreader.core.service; + +import javafx.scene.control.Alert.AlertType; +import javafx.scene.layout.StackPane; + +public interface NotificationService { + + void notify(String message, AlertType alertType, StackPane stackPane); + + void alert(String message, StackPane stackPane); + + void information(String message, StackPane stackPane); + + void warning(String message, StackPane stackPane); + + void confirmation(String message, StackPane stackPane); +} diff --git a/src/main/java/com/synops/replayreader/core/service/NotificationServiceImpl.java b/src/main/java/com/synops/replayreader/core/service/NotificationServiceImpl.java new file mode 100644 index 0000000..7d1ca23 --- /dev/null +++ b/src/main/java/com/synops/replayreader/core/service/NotificationServiceImpl.java @@ -0,0 +1,76 @@ +package com.synops.replayreader.core.service; + +import atlantafx.base.controls.Notification; +import atlantafx.base.theme.Styles; +import atlantafx.base.util.Animations; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.util.Duration; +import org.kordamp.ikonli.fluentui.FluentUiRegularAL; +import org.kordamp.ikonli.fluentui.FluentUiRegularMZ; +import org.kordamp.ikonli.javafx.FontIcon; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NotificationServiceImpl implements NotificationService { + private static final Logger LOGGER = LoggerFactory.getLogger(NotificationServiceImpl.class); + + public void notify(String message, AlertType alertType, StackPane stackPane) { + var fontIcon = switch (alertType) { + case NONE -> null; + case INFORMATION -> FluentUiRegularAL.INFO_16; + case WARNING -> FluentUiRegularMZ.WARNING_16; + case CONFIRMATION -> FluentUiRegularAL.CHECKMARK_CIRCLE_16; + case ERROR -> FluentUiRegularAL.ERROR_CIRCLE_16; + }; + + var notification = new Notification(message, fontIcon != null ? new FontIcon(fontIcon) : null); + notification.getStyleClass().add(Styles.ELEVATED_1); + notification.setPrefHeight(Region.USE_PREF_SIZE); + notification.setMaxHeight(Region.USE_PREF_SIZE); + + StackPane.setAlignment(notification, Pos.BOTTOM_RIGHT); + StackPane.setMargin(notification, new Insets(0, 10, 40, 0)); + + var style = switch (alertType) { + case NONE -> null; + case INFORMATION -> Styles.ACCENT; + case WARNING -> Styles.WARNING; + case CONFIRMATION -> Styles.SUCCESS; + case ERROR -> Styles.DANGER; + }; + + if (style != null) { + notification.getStyleClass().add(style); + } + + notification.setOnClose(_ -> { + var out = Animations.slideOutDown(notification, Duration.millis(250)); + out.setOnFinished(_ -> stackPane.getChildren().remove(notification)); + out.playFromStart(); + }); + var in = Animations.slideInDown(notification, Duration.millis(250)); + stackPane.getChildren().add(notification); + in.playFromStart(); + LOGGER.debug("Notification: {}", message); + } + + public void alert(String message, StackPane stackPane) { + notify(message, AlertType.ERROR, stackPane); + } + + public void information(String message, StackPane stackPane) { + notify(message, AlertType.INFORMATION, stackPane); + } + + public void warning(String message, StackPane stackPane) { + notify(message, AlertType.WARNING, stackPane); + } + + public void confirmation(String message, StackPane stackPane) { + notify(message, AlertType.CONFIRMATION, stackPane); + } +} diff --git a/src/main/java/com/synops/replayreader/ui/controller/MainViewController.java b/src/main/java/com/synops/replayreader/ui/controller/MainViewController.java index 75ca956..f2fdf75 100644 --- a/src/main/java/com/synops/replayreader/ui/controller/MainViewController.java +++ b/src/main/java/com/synops/replayreader/ui/controller/MainViewController.java @@ -9,6 +9,7 @@ import com.synops.replayreader.common.util.LogUtil; import com.synops.replayreader.core.event.ReplayProgressEvent; import com.synops.replayreader.core.service.DialogService; +import com.synops.replayreader.core.service.NotificationService; import com.synops.replayreader.maps.comparator.MapsComparator; import com.synops.replayreader.maps.ui.MapListCell; import com.synops.replayreader.player.comparator.PlayerListComparatorLong; @@ -40,7 +41,6 @@ import javafx.concurrent.Task; import javafx.concurrent.WorkerStateEvent; import javafx.fxml.FXML; -import javafx.scene.control.Alert.AlertType; import javafx.scene.control.ChoiceBox; import javafx.scene.control.Label; import javafx.scene.control.ListView; @@ -48,8 +48,8 @@ import javafx.scene.control.ProgressBar; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; -import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.info.BuildProperties; import org.springframework.lang.Nullable; @@ -74,12 +74,13 @@ public class MainViewController { private final StringProperty selectedVehicle = new SimpleStringProperty(""); private final StringProperty selectedClan = new SimpleStringProperty(""); private final BuildProperties buildProperties; + private final NotificationService notificationService; @Value("${replay-reader.config.max-players}") private int MAX_PLAYERS; @Value("${replay-reader.config.max-clans}") private int MAX_CLANS; @FXML - private AnchorPane root; + private StackPane root; @FXML private ChoiceBox sortingChoiceBox; @FXML @@ -158,7 +159,8 @@ public MainViewController(ReplayService replayService, @Nullable HostServices ho DialogService dialogService, UpdateClient updateClient, ResourceBundle resourceBundle, ClanStringConverter clanStringConverter, ClanListComparator clanListComparator, SortingComparators sortingComparators, MapsComparator mapsComparator, - DragDropSupport dragDropSupport, BuildProperties buildProperties) { + DragDropSupport dragDropSupport, BuildProperties buildProperties, + NotificationService notificationService) { this.replayService = replayService; this.hostServices = hostServices; this.dialogService = dialogService; @@ -170,6 +172,7 @@ public MainViewController(ReplayService replayService, @Nullable HostServices ho this.mapsComparator = mapsComparator; this.dragDropSupport = dragDropSupport; this.buildProperties = buildProperties; + this.notificationService = notificationService; } @FXML @@ -446,8 +449,7 @@ private void checkForUpdate() { MessageFormat.format(resourceBundle.getString("update.new-version"), versionResponse.tagName().substring(1)), () -> openUrl(REPLAY_RELEASES_URL)); } else { - dialogService.alert(AlertType.INFORMATION, - resourceBundle.getString("update.latest-already")); + notificationService.information(resourceBundle.getString("update.latest-already"), root); } })).doOnError(dialogService::showAlertError).subscribe(); } diff --git a/src/main/resources/views/main.fxml b/src/main/resources/views/main.fxml index add410c..97630d0 100644 --- a/src/main/resources/views/main.fxml +++ b/src/main/resources/views/main.fxml @@ -17,13 +17,13 @@ - + - + @@ -144,7 +144,7 @@ - + - + From 379988fdcedb53c4dd81bd9b0b5f3be2bf614021 Mon Sep 17 00:00:00 2001 From: synopss Date: Sat, 10 May 2025 02:12:42 +0200 Subject: [PATCH 3/3] feat: handling display of multiple notifications at once --- .../core/service/NotificationService.java | 4 +- .../core/service/NotificationServiceImpl.java | 78 +++++++++++++++---- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/synops/replayreader/core/service/NotificationService.java b/src/main/java/com/synops/replayreader/core/service/NotificationService.java index 423ae30..168b81c 100644 --- a/src/main/java/com/synops/replayreader/core/service/NotificationService.java +++ b/src/main/java/com/synops/replayreader/core/service/NotificationService.java @@ -5,8 +5,6 @@ public interface NotificationService { - void notify(String message, AlertType alertType, StackPane stackPane); - void alert(String message, StackPane stackPane); void information(String message, StackPane stackPane); @@ -14,4 +12,6 @@ public interface NotificationService { void warning(String message, StackPane stackPane); void confirmation(String message, StackPane stackPane); + + void notify(String message, AlertType alertType, StackPane stackPane); } diff --git a/src/main/java/com/synops/replayreader/core/service/NotificationServiceImpl.java b/src/main/java/com/synops/replayreader/core/service/NotificationServiceImpl.java index 7d1ca23..6e4b73f 100644 --- a/src/main/java/com/synops/replayreader/core/service/NotificationServiceImpl.java +++ b/src/main/java/com/synops/replayreader/core/service/NotificationServiceImpl.java @@ -3,6 +3,11 @@ import atlantafx.base.controls.Notification; import atlantafx.base.theme.Styles; import atlantafx.base.util.Animations; +import java.util.HashMap; +import java.util.Map; +import javafx.animation.PauseTransition; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Alert.AlertType; @@ -16,9 +21,33 @@ import org.slf4j.LoggerFactory; public class NotificationServiceImpl implements NotificationService { + private static final Logger LOGGER = LoggerFactory.getLogger(NotificationServiceImpl.class); + private static final int NOTIFICATION_SPACING = 10; + private static final int DEFAULT_BOTTOM_MARGIN = 40; + + private final Map> activeNotifications = new HashMap<>(); + + public void alert(String message, StackPane stackPane) { + notify(message, AlertType.ERROR, stackPane); + } + + public void information(String message, StackPane stackPane) { + notify(message, AlertType.INFORMATION, stackPane); + } + + public void warning(String message, StackPane stackPane) { + notify(message, AlertType.WARNING, stackPane); + } + + public void confirmation(String message, StackPane stackPane) { + notify(message, AlertType.CONFIRMATION, stackPane); + } public void notify(String message, AlertType alertType, StackPane stackPane) { + var notificationList = activeNotifications.computeIfAbsent(stackPane, + _ -> FXCollections.observableArrayList()); + var fontIcon = switch (alertType) { case NONE -> null; case INFORMATION -> FluentUiRegularAL.INFO_16; @@ -33,7 +62,12 @@ public void notify(String message, AlertType alertType, StackPane stackPane) { notification.setMaxHeight(Region.USE_PREF_SIZE); StackPane.setAlignment(notification, Pos.BOTTOM_RIGHT); - StackPane.setMargin(notification, new Insets(0, 10, 40, 0)); + + int notificationCount = notificationList.size(); + int bottomMargin = (int) (DEFAULT_BOTTOM_MARGIN + (notificationCount * ( + notification.prefHeight(-1) + NOTIFICATION_SPACING))); + + StackPane.setMargin(notification, new Insets(0, 10, bottomMargin, 0)); var style = switch (alertType) { case NONE -> null; @@ -47,30 +81,48 @@ public void notify(String message, AlertType alertType, StackPane stackPane) { notification.getStyleClass().add(style); } + notificationList.add(notification); notification.setOnClose(_ -> { - var out = Animations.slideOutDown(notification, Duration.millis(250)); - out.setOnFinished(_ -> stackPane.getChildren().remove(notification)); - out.playFromStart(); + removeNotification(notification, stackPane); }); var in = Animations.slideInDown(notification, Duration.millis(250)); stackPane.getChildren().add(notification); in.playFromStart(); LOGGER.debug("Notification: {}", message); - } - public void alert(String message, StackPane stackPane) { - notify(message, AlertType.ERROR, stackPane); + scheduleAutoClose(notification, stackPane); } - public void information(String message, StackPane stackPane) { - notify(message, AlertType.INFORMATION, stackPane); + private void removeNotification(Notification notification, StackPane stackPane) { + var notificationList = activeNotifications.get(stackPane); + if (notificationList == null) { + return; + } + + var out = Animations.slideOutDown(notification, Duration.millis(250)); + out.setOnFinished(_ -> { + stackPane.getChildren().remove(notification); + notificationList.remove(notification); + repositionNotifications(notificationList); + }); + out.playFromStart(); } - public void warning(String message, StackPane stackPane) { - notify(message, AlertType.WARNING, stackPane); + private void repositionNotifications(ObservableList notificationList) { + for (int i = 0; i < notificationList.size(); i++) { + var notification = notificationList.get(i); + int bottomMargin = (int) (DEFAULT_BOTTOM_MARGIN + (i * (notification.prefHeight(-1) + + NOTIFICATION_SPACING))); + + var moveUp = Animations.fadeIn(notification, Duration.millis(150)); + StackPane.setMargin(notification, new Insets(0, 10, bottomMargin, 0)); + moveUp.playFromStart(); + } } - public void confirmation(String message, StackPane stackPane) { - notify(message, AlertType.CONFIRMATION, stackPane); + private void scheduleAutoClose(Notification notification, StackPane stackPane) { + var delay = new PauseTransition(Duration.seconds(5)); + delay.setOnFinished(_ -> removeNotification(notification, stackPane)); + delay.play(); } }