initial commit

This commit is contained in:
2026-05-11 10:01:56 +02:00
commit 19ecbf1955
18 changed files with 624 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+58
View File
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cz.jzitnik</groupId>
<artifactId>jecnaclient</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<!-- Make sure this matches your exact package and class name -->
<mainClass>cz.jzitnik.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- JavaFX Controls -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>21.0.1</version> <!-- Use the version matching your JDK -->
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>21.0.1</version>
</dependency>
<!-- AtlantaFX Theme -->
<dependency>
<groupId>io.github.mkpaz</groupId>
<artifactId>atlantafx-base</artifactId>
<version>2.0.1</version>
</dependency>
<!-- Reflections -->
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
</dependencies>
</project>
+50
View File
@@ -0,0 +1,50 @@
package cz.jzitnik;
import atlantafx.base.theme.PrimerDark;
import cz.jzitnik.state.StateManager;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage stage) throws Exception {
Application.setUserAgentStylesheet(new PrimerDark().getUserAgentStylesheet());
// 1. Initialize State Manager
// This will find all @State annotated classes inside the cz.jzitnik package and create singletons
StateManager.getInstance().initialize("cz.jzitnik");
// 2. Set up routing container (this is our root)
StackPane root = new StackPane();
root.setStyle("-fx-background-color: -color-bg-default;");
// Ensure contents never bleed out and cause weird window scaling
Rectangle clip = new Rectangle();
clip.widthProperty().bind(root.widthProperty());
clip.heightProperty().bind(root.heightProperty());
root.setClip(clip);
// 3. Initialize Router
cz.jzitnik.router.Router router = cz.jzitnik.router.Router.getInstance();
router.setRootContainer(root);
router.registerRoute("/login", cz.jzitnik.controllers.LoginController.class);
router.registerRoute("/home", cz.jzitnik.controllers.HomeController.class);
// Initial screen
router.navigate("/login");
// 4. Display
Scene scene = new Scene(root, 800, 600);
stage.setTitle("JecnaClient");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
@@ -0,0 +1,40 @@
package cz.jzitnik.controllers;
import cz.jzitnik.router.Routable;
import cz.jzitnik.router.Route;
import cz.jzitnik.router.Router;
import cz.jzitnik.state.AppState;
import cz.jzitnik.state.InjectState;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import java.util.Map;
@Route(fxml = "/home.fxml")
public class HomeController implements Routable {
@InjectState
private AppState appState;
@FXML
private Label welcomeLabel;
@Override
public void onNavigate(Map<String, Object> props) {
// Because of dependency injection, appState is already fully populated
String username = appState.getUsername();
if (username == null) username = "User";
welcomeLabel.setText("Welcome, " + username + "!");
System.out.println("Global Auth Token is: " + appState.getToken());
}
@FXML
public void onLogoutClick() {
// Clear global state
appState.clear();
// Redirect
Router.getInstance().navigate("/login");
}
}
@@ -0,0 +1,46 @@
package cz.jzitnik.controllers;
import cz.jzitnik.router.Routable;
import cz.jzitnik.router.Route;
import cz.jzitnik.router.Router;
import cz.jzitnik.state.AppState;
import cz.jzitnik.state.InjectState;
import javafx.fxml.FXML;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import java.util.Map;
@Route(fxml = "/login.fxml")
public class LoginController implements Routable {
@InjectState
private AppState appState;
@FXML
private TextField usernameField;
@FXML
private PasswordField passwordField;
@Override
public void onNavigate(Map<String, Object> props) {
// Here you can use props if the screen was opened with them
}
@FXML
public void onLoginClick() {
String username = usernameField.getText();
// Save the username globally into the app state
if (username != null && !username.isEmpty()) {
appState.setUsername(username);
appState.setToken("dummy_token_123");
} else {
appState.setUsername("Guest");
}
// Redirect to the home screen (no longer need to pass username in props, it's global now!)
Router.getInstance().navigate("/home");
}
}
@@ -0,0 +1,10 @@
package cz.jzitnik.router;
import java.util.Map;
/**
* Interface for screen controllers that can receive properties when navigated to.
*/
public interface Routable {
default void onNavigate(Map<String, Object> props) {}
}
@@ -0,0 +1,15 @@
package cz.jzitnik.router;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to map a controller class to an FXML file.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Route {
String fxml();
}
@@ -0,0 +1,75 @@
package cz.jzitnik.router;
import cz.jzitnik.state.StateManager;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.layout.Pane;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* Scalable API for managing screens.
*/
public class Router {
private static Router instance;
private Pane rootContainer;
private final Map<String, Class<?>> routes = new HashMap<>();
private Router() {}
public static Router getInstance() {
if (instance == null) {
instance = new Router();
}
return instance;
}
public void setRootContainer(Pane rootContainer) {
this.rootContainer = rootContainer;
}
public void registerRoute(String path, Class<?> controllerClass) {
if (!controllerClass.isAnnotationPresent(Route.class)) {
throw new IllegalArgumentException("Controller " + controllerClass.getName() + " must be annotated with @Route");
}
routes.put(path, controllerClass);
}
public void navigate(String path) {
navigate(path, new HashMap<>());
}
public void navigate(String path, Map<String, Object> props) {
if (rootContainer == null) {
throw new IllegalStateException("Root container not set in Router.");
}
Class<?> controllerClass = routes.get(path);
if (controllerClass == null) {
throw new IllegalArgumentException("Route not found: " + path);
}
Route routeAnnotation = controllerClass.getAnnotation(Route.class);
String fxmlPath = routeAnnotation.fxml();
try {
FXMLLoader loader = new FXMLLoader(controllerClass.getResource(fxmlPath));
Parent view = loader.load();
Object controller = loader.getController();
// Inject global state into the controller if it uses @InjectState
StateManager.getInstance().injectStates(controller);
if (controller instanceof Routable routable) {
routable.onNavigate(props);
}
rootContainer.getChildren().clear();
rootContainer.getChildren().add(view);
} catch (IOException e) {
throw new RuntimeException("Failed to load screen for route: " + path, e);
}
}
}
@@ -0,0 +1,33 @@
package cz.jzitnik.state;
@State
public class AppState {
private String username;
private String token; // Example for some auth token
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public boolean isLoggedIn() {
return username != null && !username.isEmpty();
}
public void clear() {
this.username = null;
this.token = null;
}
}
@@ -0,0 +1,14 @@
package cz.jzitnik.state;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Injects a singleton state object into a controller field.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectState {
}
+15
View File
@@ -0,0 +1,15 @@
package cz.jzitnik.state;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a class as a managed state object.
* The StateManager will instantiate and hold a singleton of this class.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface State {
}
@@ -0,0 +1,73 @@
package cz.jzitnik.state;
import org.reflections.Reflections;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Manages @State annotated classes, storing them as singletons and injecting them.
*/
public class StateManager {
private static StateManager instance;
private final Map<Class<?>, Object> states = new HashMap<>();
private StateManager() {
}
public static StateManager getInstance() {
if (instance == null) {
instance = new StateManager();
}
return instance;
}
/**
* Scans the specified base package for @State annotated classes and instantiates them.
* @param basePackage the base package to scan (e.g. "cz.jzitnik")
*/
public void initialize(String basePackage) {
Reflections reflections = new Reflections(basePackage);
Set<Class<?>> stateClasses = reflections.getTypesAnnotatedWith(State.class);
for (Class<?> stateClass : stateClasses) {
try {
Object stateInstance = stateClass.getDeclaredConstructor().newInstance();
states.put(stateClass, stateInstance);
} catch (Exception e) {
System.err.println("Failed to instantiate state class: " + stateClass.getName());
e.printStackTrace();
}
}
}
/**
* Injects initialized states into fields annotated with @InjectState in the given target object.
* @param target the controller or object to inject state into
*/
public void injectStates(Object target) {
if (target == null) return;
Class<?> targetClass = target.getClass();
for (Field field : targetClass.getDeclaredFields()) {
if (field.isAnnotationPresent(InjectState.class)) {
Class<?> fieldType = field.getType();
Object stateInstance = states.get(fieldType);
if (stateInstance == null) {
throw new RuntimeException("No @State found for type: " + fieldType.getName() + " in " + targetClass.getName());
}
try {
field.setAccessible(true);
field.set(target, stateInstance);
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to inject state into field: " + field.getName(), e);
}
}
}
}
}
@@ -0,0 +1,68 @@
package cz.jzitnik.ui;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.effect.BlendMode;
import javafx.scene.effect.GaussianBlur;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.util.Duration;
/**
* Reusable animated background component.
*/
public class AuroraBackground extends StackPane {
public AuroraBackground() {
// LOWER OPACITY: Dropped from 0.3 to 0.1 for a subtle background feel
Color color1 = Color.web("#00FFFF", 0.10); // Cyan
Color color2 = Color.web("#FF00FF", 0.10); // Magenta
Color color3 = Color.web("#7D3CFF", 0.10); // Deep Purple
// MASSIVE BLOBS: Increased radius so they fill large screens beautifully
Circle blob1 = new Circle(400, color1);
Circle blob2 = new Circle(350, color2);
Circle blob3 = new Circle(450, color3);
// HEAVY BLUR: Increased from 100 to 200 for maximum softness
GaussianBlur blur = new GaussianBlur(200);
blob1.setEffect(blur);
blob2.setEffect(blur);
blob3.setEffect(blur);
blob1.setBlendMode(BlendMode.ADD);
blob2.setBlendMode(BlendMode.ADD);
blob3.setBlendMode(BlendMode.ADD);
// Because this is a StackPane, all circles start perfectly centered
getChildren().addAll(blob1, blob2, blob3);
// Animate them drifting away from the center using translation
setupAnimation(blob1, -150, -100, 150, 100, 20);
setupAnimation(blob2, 150, 150, -150, -150, 25);
setupAnimation(blob3, 0, -200, 0, 200, 15);
}
/**
* Animates a node shifting back and forth from a center point.
*/
private void setupAnimation(Circle node, double startX, double startY, double endX, double endY, int durationSeconds) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(node.translateXProperty(), startX),
new KeyValue(node.translateYProperty(), startY)
),
new KeyFrame(Duration.seconds(durationSeconds),
new KeyValue(node.translateXProperty(), endX),
new KeyValue(node.translateYProperty(), endY)
)
);
timeline.setAutoReverse(true);
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
}
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.StackPane?>
<!-- Home screen with a solid transparent/default background (NO aurora background here) -->
<StackPane xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="cz.jzitnik.controllers.HomeController">
<VBox alignment="CENTER" spacing="15.0" maxWidth="350.0"
style="-fx-background-color: transparent;">
<padding>
<Insets bottom="40.0" left="40.0" right="40.0" top="40.0"/>
</padding>
<Label text="Home Screen" styleClass="title-1"/>
<Label fx:id="welcomeLabel" styleClass="text-muted"/>
<Button text="Logout" onAction="#onLogoutClick" maxWidth="Infinity" styleClass="danger"/>
</VBox>
</StackPane>
+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.StackPane?>
<?import cz.jzitnik.ui.AuroraBackground?>
<StackPane xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="cz.jzitnik.controllers.LoginController">
<!-- Screen Specific Background -->
<AuroraBackground />
<!-- Actual Content -->
<VBox alignment="CENTER" spacing="15.0" maxWidth="350.0"
style="-fx-background-color: transparent;">
<padding>
<Insets bottom="40.0" left="40.0" right="40.0" top="40.0"/>
</padding>
<VBox alignment="CENTER" spacing="5.0">
<Label text="Sign In" styleClass="title-2"/>
<Label text="Welcome back to the portal" styleClass="text-muted"/>
</VBox>
<Label prefHeight="15.0"/>
<TextField fx:id="usernameField" promptText="Username or Email"/>
<PasswordField fx:id="passwordField" promptText="Password"/>
<Button text="Login" onAction="#onLoginClick" maxWidth="Infinity" styleClass="accent"/>
</VBox>
</StackPane>