initial commit
This commit is contained in:
@@ -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 {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user