commit 19ecbf19556034fba1e57dc8e80c4ee19eb5c878 Author: jzitnik-dev Date: Mon May 11 10:01:56 2026 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..480bdf5 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..fdc35ea --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6f35dc0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + cz.jzitnik + jecnaclient + 1.0-SNAPSHOT + + + 21 + 21 + UTF-8 + + + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + + cz.jzitnik.Main + + + + + + + + + org.openjfx + javafx-controls + 21.0.1 + + + org.openjfx + javafx-fxml + 21.0.1 + + + + + io.github.mkpaz + atlantafx-base + 2.0.1 + + + + + org.reflections + reflections + 0.10.2 + + + \ No newline at end of file diff --git a/src/main/java/cz/jzitnik/Main.java b/src/main/java/cz/jzitnik/Main.java new file mode 100644 index 0000000..469e0ef --- /dev/null +++ b/src/main/java/cz/jzitnik/Main.java @@ -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); + } +} diff --git a/src/main/java/cz/jzitnik/controllers/HomeController.java b/src/main/java/cz/jzitnik/controllers/HomeController.java new file mode 100644 index 0000000..7334aff --- /dev/null +++ b/src/main/java/cz/jzitnik/controllers/HomeController.java @@ -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 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"); + } +} diff --git a/src/main/java/cz/jzitnik/controllers/LoginController.java b/src/main/java/cz/jzitnik/controllers/LoginController.java new file mode 100644 index 0000000..b18dd98 --- /dev/null +++ b/src/main/java/cz/jzitnik/controllers/LoginController.java @@ -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 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"); + } +} diff --git a/src/main/java/cz/jzitnik/router/Routable.java b/src/main/java/cz/jzitnik/router/Routable.java new file mode 100644 index 0000000..2e81dc8 --- /dev/null +++ b/src/main/java/cz/jzitnik/router/Routable.java @@ -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 props) {} +} diff --git a/src/main/java/cz/jzitnik/router/Route.java b/src/main/java/cz/jzitnik/router/Route.java new file mode 100644 index 0000000..021e150 --- /dev/null +++ b/src/main/java/cz/jzitnik/router/Route.java @@ -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(); +} diff --git a/src/main/java/cz/jzitnik/router/Router.java b/src/main/java/cz/jzitnik/router/Router.java new file mode 100644 index 0000000..f3812d4 --- /dev/null +++ b/src/main/java/cz/jzitnik/router/Router.java @@ -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> 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 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); + } + } +} diff --git a/src/main/java/cz/jzitnik/state/AppState.java b/src/main/java/cz/jzitnik/state/AppState.java new file mode 100644 index 0000000..385ec10 --- /dev/null +++ b/src/main/java/cz/jzitnik/state/AppState.java @@ -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; + } +} diff --git a/src/main/java/cz/jzitnik/state/InjectState.java b/src/main/java/cz/jzitnik/state/InjectState.java new file mode 100644 index 0000000..a9ee2e0 --- /dev/null +++ b/src/main/java/cz/jzitnik/state/InjectState.java @@ -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 { +} diff --git a/src/main/java/cz/jzitnik/state/State.java b/src/main/java/cz/jzitnik/state/State.java new file mode 100644 index 0000000..e4410ce --- /dev/null +++ b/src/main/java/cz/jzitnik/state/State.java @@ -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 { +} diff --git a/src/main/java/cz/jzitnik/state/StateManager.java b/src/main/java/cz/jzitnik/state/StateManager.java new file mode 100644 index 0000000..40afcd4 --- /dev/null +++ b/src/main/java/cz/jzitnik/state/StateManager.java @@ -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, 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> 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); + } + } + } + } +} diff --git a/src/main/java/cz/jzitnik/ui/AuroraBackground.java b/src/main/java/cz/jzitnik/ui/AuroraBackground.java new file mode 100644 index 0000000..ddd158b --- /dev/null +++ b/src/main/java/cz/jzitnik/ui/AuroraBackground.java @@ -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(); + } +} diff --git a/src/main/resources/home.fxml b/src/main/resources/home.fxml new file mode 100644 index 0000000..e5f0e24 --- /dev/null +++ b/src/main/resources/home.fxml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + +