docs: Added some javadoc

This commit is contained in:
2026-05-31 16:42:22 +02:00
parent 64a11aee26
commit bac9272401
23 changed files with 572 additions and 16 deletions
+19
View File
@@ -19,8 +19,22 @@ import java.util.Optional;
import cz.jzitnik.query.QueryClient; import cz.jzitnik.query.QueryClient;
import cz.jzitnik.query.QueryOptions; import cz.jzitnik.query.QueryOptions;
/**
* The main entry point for the JecnaClient desktop application.
* This class initializes the application's UI theme, sets up the navigation router,
* and handles the initial application state, including attempting to restore
* a user's session from previously saved credentials.
*/
public class Main extends Application { public class Main extends Application {
/**
* Initializes the JavaFX application, configures the UI, and sets up the routing system.
* It attempts to automatically log the user in if stored credentials are available,
* navigating to the dashboard on success or to the login screen if authentication fails.
*
* @param stage the primary window for the application.
* @throws Exception if an error occurs during initialization.
*/
@Override @Override
public void start(Stage stage) throws Exception { public void start(Stage stage) throws Exception {
Application.setUserAgentStylesheet(new PrimerDark().getUserAgentStylesheet()); Application.setUserAgentStylesheet(new PrimerDark().getUserAgentStylesheet());
@@ -77,6 +91,11 @@ public class Main extends Application {
} }
} }
/**
* Standard entry point for launching the JavaFX application.
*
* @param args command-line arguments passed to the application.
*/
public static void main(String[] args) { public static void main(String[] args) {
launch(args); launch(args);
} }
@@ -1,7 +1,3 @@
/*
AI GENERATED SLOP
*/
package cz.jzitnik.auth; package cz.jzitnik.auth;
import javax.crypto.Cipher; import javax.crypto.Cipher;
@@ -19,6 +15,9 @@ import java.util.EnumSet;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
/**
* Securely stores and retrieves user credentials using AES-GCM encryption.
*/
public class CredentialStore { public class CredentialStore {
private static final Path DIR = Paths.get(System.getProperty("user.home"), ".jecnaclient"); private static final Path DIR = Paths.get(System.getProperty("user.home"), ".jecnaclient");
private static final Path KEY_FILE = DIR.resolve("secret.key"); private static final Path KEY_FILE = DIR.resolve("secret.key");
@@ -26,16 +25,32 @@ public class CredentialStore {
private static final int KEY_SIZE = 256; private static final int KEY_SIZE = 256;
private static final SecureRandom RAND = new SecureRandom(); private static final SecureRandom RAND = new SecureRandom();
/**
* Represents user credentials (username and password).
*/
public static final class Credentials { public static final class Credentials {
public final String username; public final String username;
public final String password; public final String password;
/**
* Creates a new Credentials object.
*
* @param u The username.
* @param p The password.
*/
public Credentials(String u, String p) { public Credentials(String u, String p) {
this.username = u; this.username = u;
this.password = p; this.password = p;
} }
} }
/**
* Saves user credentials to a secure file.
*
* @param username The username to save.
* @param password The password to save.
* @throws Exception If an error occurs during saving.
*/
public static void saveCredentials(String username, String password) throws Exception { public static void saveCredentials(String username, String password) throws Exception {
if (username == null || password == null) return; if (username == null || password == null) return;
ensureDir(); ensureDir();
@@ -60,6 +75,12 @@ public class CredentialStore {
trySetOwnerOnly(CRED_FILE); trySetOwnerOnly(CRED_FILE);
} }
/**
* Loads user credentials from the secure file.
*
* @return An Optional containing the credentials if found and decrypted successfully, or empty otherwise.
* @throws Exception If an error occurs during loading or decryption.
*/
public static Optional<Credentials> loadCredentials() throws Exception { public static Optional<Credentials> loadCredentials() throws Exception {
if (!Files.exists(CRED_FILE)) return Optional.empty(); if (!Files.exists(CRED_FILE)) return Optional.empty();
if (!Files.exists(KEY_FILE)) return Optional.empty(); if (!Files.exists(KEY_FILE)) return Optional.empty();
@@ -87,12 +108,18 @@ public class CredentialStore {
return Optional.of(new Credentials(u, p)); return Optional.of(new Credentials(u, p));
} }
/**
* Clears stored credentials by deleting the encrypted file.
*/
public static void clearCredentials() { public static void clearCredentials() {
try { try {
Files.deleteIfExists(CRED_FILE); Files.deleteIfExists(CRED_FILE);
} catch (IOException ignored) {} } catch (IOException ignored) {}
} }
/**
* Ensures the credentials directory exists with appropriate permissions.
*/
private static void ensureDir() throws IOException { private static void ensureDir() throws IOException {
if (!Files.exists(DIR)) { if (!Files.exists(DIR)) {
Files.createDirectories(DIR); Files.createDirectories(DIR);
@@ -100,6 +127,9 @@ public class CredentialStore {
} }
} }
/**
* Loads the encryption key or creates a new one if it doesn't exist.
*/
private static SecretKey loadOrCreateKey() throws Exception { private static SecretKey loadOrCreateKey() throws Exception {
if (Files.exists(KEY_FILE)) { if (Files.exists(KEY_FILE)) {
byte[] k = Files.readAllBytes(KEY_FILE); byte[] k = Files.readAllBytes(KEY_FILE);
@@ -113,6 +143,9 @@ public class CredentialStore {
return key; return key;
} }
/**
* Best-effort attempt to set POSIX file permissions to owner-only.
*/
private static void trySetOwnerOnly(Path p) { private static void trySetOwnerOnly(Path p) {
try { try {
// POSIX systems // POSIX systems
@@ -18,6 +18,10 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* Controller for handling the absences view.
* Displays student absences, including a summary and detailed breakdown by date.
*/
@Route(path = "/absences", fxml = "/absences.fxml") @Route(path = "/absences", fxml = "/absences.fxml")
public class AbsencesController extends DashboardBaseController { public class AbsencesController extends DashboardBaseController {
@@ -35,12 +39,22 @@ public class AbsencesController extends DashboardBaseController {
private AbsencesPage currentPage; private AbsencesPage currentPage;
/**
* Initializes the controller when navigating to this view.
* Initiates loading of absence data.
*
* @param props Navigation properties, not explicitly used here.
*/
@Override @Override
public void onNavigate(Map<String, Object> props) { public void onNavigate(Map<String, Object> props) {
super.onNavigate(props); super.onNavigate(props);
loadAbsences(); loadAbsences();
} }
/**
* Fetches the absences page from the API and updates the UI.
* Shows a loading indicator while fetching.
*/
private void loadAbsences() { private void loadAbsences() {
loadingIndicator.setVisible(true); loadingIndicator.setVisible(true);
scrollPane.setVisible(false); scrollPane.setVisible(false);
@@ -57,6 +71,11 @@ public class AbsencesController extends DashboardBaseController {
}, QueryOptions.defaultOptions()).thenAccept(this::handleAbsencesResult); }, QueryOptions.defaultOptions()).thenAccept(this::handleAbsencesResult);
} }
/**
* Handles the result of the absences fetch operation.
*
* @param result The result of the fetch.
*/
private void handleAbsencesResult(QueryResult<AbsencesPage> result) { private void handleAbsencesResult(QueryResult<AbsencesPage> result) {
Platform.runLater(() -> { Platform.runLater(() -> {
loadingIndicator.setVisible(false); loadingIndicator.setVisible(false);
@@ -75,6 +94,9 @@ public class AbsencesController extends DashboardBaseController {
}); });
} }
/**
* Renders the absence summary and detail cards.
*/
private void renderAbsences() { private void renderAbsences() {
summaryBox.getChildren().clear(); summaryBox.getChildren().clear();
detailFlow.getChildren().clear(); detailFlow.getChildren().clear();
@@ -111,6 +133,14 @@ public class AbsencesController extends DashboardBaseController {
); );
} }
/**
* Creates a summary card UI component.
*
* @param title The title of the summary.
* @param count The numerical value to display.
* @param styleClass The CSS style class to apply for coloring.
* @return The created VBox component.
*/
private VBox createSummaryCard(String title, int count, String styleClass) { private VBox createSummaryCard(String title, int count, String styleClass) {
VBox card = new VBox(5); VBox card = new VBox(5);
card.setAlignment(Pos.CENTER); card.setAlignment(Pos.CENTER);
@@ -126,6 +156,13 @@ public class AbsencesController extends DashboardBaseController {
return card; return card;
} }
/**
* Creates an absence detail card UI component for a specific day.
*
* @param day The date of the absence.
* @param info The absence information for the day.
* @return The created VBox component.
*/
private VBox createAbsenceCard(LocalDate day, AbsenceInfo info) { private VBox createAbsenceCard(LocalDate day, AbsenceInfo info) {
VBox card = new VBox(8); VBox card = new VBox(8);
card.getStyleClass().addAll("card", "absence-card"); card.getStyleClass().addAll("card", "absence-card");
@@ -162,11 +199,20 @@ public class AbsencesController extends DashboardBaseController {
return card; return card;
} }
/**
* Formats a date for display.
*
* @param date The date to format.
* @return The formatted date string.
*/
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private String formatDate(LocalDate date) { private String formatDate(LocalDate date) {
return date.getDayOfMonth() + ". " + date.getMonthNumber() + ". " + date.getYear(); return date.getDayOfMonth() + ". " + date.getMonthNumber() + ". " + date.getYear();
} }
/**
* Handles navigation back to the dashboard.
*/
@FXML @FXML
protected void onBackToDashboard() { protected void onBackToDashboard() {
Router.getInstance().navigate("/dashboard"); Router.getInstance().navigate("/dashboard");
@@ -12,6 +12,11 @@ import javafx.scene.layout.*;
import java.util.Map; import java.util.Map;
/**
* Controller responsible for displaying detailed information about a specific classroom.
* This includes fetching room data from the Jecna API and rendering the room's details,
* such as its name, code, floor, and timetable.
*/
@Route(path = "/classroom_detail", fxml = "/classroom_detail.fxml") @Route(path = "/classroom_detail", fxml = "/classroom_detail.fxml")
public class ClassroomDetailController extends DashboardBaseController { public class ClassroomDetailController extends DashboardBaseController {
@@ -32,6 +37,11 @@ public class ClassroomDetailController extends DashboardBaseController {
private String roomCode; private String roomCode;
/**
* Initializes the view with the specified room code.
*
* @param props a map containing the "code" of the room to display.
*/
@Override @Override
public void onNavigate(Map<String, Object> props) { public void onNavigate(Map<String, Object> props) {
super.onNavigate(props); super.onNavigate(props);
@@ -45,6 +55,12 @@ public class ClassroomDetailController extends DashboardBaseController {
loadRoom(); loadRoom();
} }
@FXML
@Override
protected void onBack() {
Router.getInstance().navigate("/classrooms");
}
private void loadRoom() { private void loadRoom() {
loadingIndicator.setVisible(true); loadingIndicator.setVisible(true);
scrollPane.setVisible(false); scrollPane.setVisible(false);
@@ -121,10 +137,4 @@ public class ClassroomDetailController extends DashboardBaseController {
return row + 1; return row + 1;
} }
@FXML
@Override
protected void onBack() {
Router.getInstance().navigate("/classrooms");
}
} }
@@ -16,6 +16,11 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* Controller for the classrooms view, which displays a list of available classrooms.
* Users can search and filter classrooms, and clicking a classroom navigates
* to its detailed view.
*/
@Route(path = "/classrooms", fxml = "/classrooms.fxml") @Route(path = "/classrooms", fxml = "/classrooms.fxml")
public class ClassroomsController extends DashboardBaseController { public class ClassroomsController extends DashboardBaseController {
@@ -33,6 +38,11 @@ public class ClassroomsController extends DashboardBaseController {
private List<RoomReference> allRooms = new ArrayList<>(); private List<RoomReference> allRooms = new ArrayList<>();
/**
* Initializes the view, sets up the search filter, and triggers loading of the classrooms.
*
* @param props navigation properties.
*/
@Override @Override
public void onNavigate(Map<String, Object> props) { public void onNavigate(Map<String, Object> props) {
super.onNavigate(props); super.onNavigate(props);
@@ -115,6 +125,9 @@ public class ClassroomsController extends DashboardBaseController {
} }
} }
/**
* Navigates back to the dashboard.
*/
@FXML @FXML
protected void onBackToDashboard() { protected void onBackToDashboard() {
Router.getInstance().navigate("/dashboard"); Router.getInstance().navigate("/dashboard");
@@ -12,6 +12,10 @@ import cz.jzitnik.router.Router;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Button; import javafx.scene.control.Button;
/**
* Base controller for dashboard views.
* Handles common functionality such as navigation and loading user profile information.
*/
public class DashboardBaseController implements Routable { public class DashboardBaseController implements Routable {
@InjectState @InjectState
@@ -48,9 +52,18 @@ public class DashboardBaseController implements Routable {
protected void onDoNothing() { protected void onDoNothing() {
} }
/**
* Handles navigation back to the home view.
*/
@FXML @FXML
protected void onBack() { Router.getInstance().navigate("/home"); } protected void onBack() { Router.getInstance().navigate("/home"); }
/**
* Called when navigating to a dashboard view.
* Initializes the welcome and class labels and fetches user profile data.
*
* @param props Navigation properties.
*/
@Override @Override
public void onNavigate(Map<String, Object> props) { public void onNavigate(Map<String, Object> props) {
if (welcomeLabel != null && classLabel != null) { if (welcomeLabel != null && classLabel != null) {
@@ -7,14 +7,26 @@ import javafx.fxml.FXML;
import java.util.Map; import java.util.Map;
/**
* Controller for handling the main dashboard view.
*/
@Route(path = "/dashboard", fxml = "/dashboard_modern.fxml") @Route(path = "/dashboard", fxml = "/dashboard_modern.fxml")
public class DashboardController extends DashboardBaseController { public class DashboardController extends DashboardBaseController {
/**
* Called when navigating to the dashboard view.
*
* @param props Navigation properties.
*/
@Override @Override
public void onNavigate(Map<String, Object> props) { public void onNavigate(Map<String, Object> props) {
super.onNavigate(props); super.onNavigate(props);
} }
/**
* Handles the logout action.
* Clears application state, credentials, and navigates back to the login screen.
*/
@FXML @FXML
public void onLogoutClick() { public void onLogoutClick() {
appState.clear(); appState.clear();
@@ -20,6 +20,10 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* Controller for handling the grades view.
* Displays student grades grouped by subject and allows grade prediction.
*/
@Route(path = "/grades", fxml = "/grades.fxml") @Route(path = "/grades", fxml = "/grades.fxml")
public class GradesController extends DashboardBaseController { public class GradesController extends DashboardBaseController {
@@ -35,8 +39,20 @@ public class GradesController extends DashboardBaseController {
private GradesPage currentPage; private GradesPage currentPage;
private final Map<String, List<PredictedGrade>> predictions = new HashMap<>(); private final Map<String, List<PredictedGrade>> predictions = new HashMap<>();
/**
* Represents a predicted grade.
*
* @param value The grade value (1-5).
* @param isSmall Whether the grade has a smaller weight.
*/
public record PredictedGrade(int value, boolean isSmall) {} public record PredictedGrade(int value, boolean isSmall) {}
/**
* Initializes the controller when navigating to this view.
* Clears previous predictions and initiates loading of grades data.
*
* @param props Navigation properties, not explicitly used here.
*/
@Override @Override
public void onNavigate(Map<String, Object> props) { public void onNavigate(Map<String, Object> props) {
super.onNavigate(props); super.onNavigate(props);
@@ -44,6 +60,10 @@ public class GradesController extends DashboardBaseController {
loadGrades(); loadGrades();
} }
/**
* Fetches the grades page from the API and updates the UI.
* Shows a loading indicator while fetching.
*/
private void loadGrades() { private void loadGrades() {
loadingIndicator.setVisible(true); loadingIndicator.setVisible(true);
scrollPane.setVisible(false); scrollPane.setVisible(false);
@@ -59,6 +79,11 @@ public class GradesController extends DashboardBaseController {
}, QueryOptions.defaultOptions()).thenAccept(this::handleGradesResult); }, QueryOptions.defaultOptions()).thenAccept(this::handleGradesResult);
} }
/**
* Handles the result of the grades fetch operation.
*
* @param result The result of the fetch.
*/
private void handleGradesResult(QueryResult<GradesPage> result) { private void handleGradesResult(QueryResult<GradesPage> result) {
Platform.runLater(() -> { Platform.runLater(() -> {
loadingIndicator.setVisible(false); loadingIndicator.setVisible(false);
@@ -78,6 +103,9 @@ public class GradesController extends DashboardBaseController {
}); });
} }
/**
* Renders all subjects and their grades.
*/
private void renderAllSubjects() { private void renderAllSubjects() {
contentBox.getChildren().clear(); contentBox.getChildren().clear();
contentBox.setAlignment(Pos.TOP_CENTER); contentBox.setAlignment(Pos.TOP_CENTER);
@@ -95,6 +123,13 @@ public class GradesController extends DashboardBaseController {
} }
} }
/**
* Calculates the average grade for a subject, including predictions.
*
* @param subject The subject.
* @param preds List of predicted grades.
* @return The calculated average, or null if no grades.
*/
private Float calculateAverage(Subject subject, List<PredictedGrade> preds) { private Float calculateAverage(Subject subject, List<PredictedGrade> preds) {
float sum = 0; float sum = 0;
float count = 0; float count = 0;
@@ -122,6 +157,12 @@ public class GradesController extends DashboardBaseController {
return count > 0 ? sum / count : null; return count > 0 ? sum / count : null;
} }
/**
* Creates a subject card UI component.
*
* @param subject The subject.
* @return The created VBox component.
*/
private VBox createSubjectCard(Subject subject) { private VBox createSubjectCard(Subject subject) {
String subjectNameFull = subject.getName().getFull(); String subjectNameFull = subject.getName().getFull();
List<PredictedGrade> preds = predictions.getOrDefault(subjectNameFull, new ArrayList<>()); List<PredictedGrade> preds = predictions.getOrDefault(subjectNameFull, new ArrayList<>());
@@ -208,6 +249,12 @@ public class GradesController extends DashboardBaseController {
return card; return card;
} }
/**
* Shows a dialog to add a predicted grade.
*
* @param subjectNameFull Full subject name.
* @param preds List of existing predictions for the subject.
*/
private void showPredictionDialog(String subjectNameFull, List<PredictedGrade> preds) { private void showPredictionDialog(String subjectNameFull, List<PredictedGrade> preds) {
Dialog<PredictedGrade> dialog = new Dialog<>(); Dialog<PredictedGrade> dialog = new Dialog<>();
dialog.setTitle("Nová predikce"); dialog.setTitle("Nová predikce");
@@ -247,6 +294,18 @@ public class GradesController extends DashboardBaseController {
}); });
} }
/**
* Creates a grade badge UI component.
*
* @param value The grade value.
* @param isSmall Whether it's a small grade.
* @param valueChar The character representation of the grade (if not numeric).
* @param isPredicted Whether this is a predicted grade.
* @param desc Description of the grade.
* @param teacher Teacher who gave the grade.
* @param date Date the grade was received.
* @return The created StackPane component.
*/
private StackPane createGradeBadge(int value, boolean isSmall, char valueChar, boolean isPredicted, String desc, String teacher, String date) { private StackPane createGradeBadge(int value, boolean isSmall, char valueChar, boolean isPredicted, String desc, String teacher, String date) {
StackPane badge = new StackPane(); StackPane badge = new StackPane();
String valStr = String.valueOf(valueChar); String valStr = String.valueOf(valueChar);
@@ -302,6 +361,13 @@ public class GradesController extends DashboardBaseController {
return badge; return badge;
} }
/**
* Shows a dialog with grade details.
*
* @param desc Description of the grade.
* @param teacher Teacher who gave the grade.
* @param date Date the grade was received.
*/
private void showGradeDetailsDialog(String desc, String teacher, String date) { private void showGradeDetailsDialog(String desc, String teacher, String date) {
Dialog<Void> dialog = new Dialog<>(); Dialog<Void> dialog = new Dialog<>();
dialog.setTitle("Detail známky"); dialog.setTitle("Detail známky");
@@ -325,6 +391,9 @@ public class GradesController extends DashboardBaseController {
dialog.show(); dialog.show();
} }
/**
* Handles navigation back to the dashboard.
*/
@FXML @FXML
protected void onBackToDashboard() { protected void onBackToDashboard() {
Router.getInstance().navigate("/dashboard"); Router.getInstance().navigate("/dashboard");
@@ -10,6 +10,9 @@ import javafx.scene.control.Label;
import java.util.Map; import java.util.Map;
/**
* Controller for handling the home view.
*/
@Route(path = "/home", fxml = "/home.fxml") @Route(path = "/home", fxml = "/home.fxml")
public class HomeController implements Routable { public class HomeController implements Routable {
@@ -19,6 +22,12 @@ public class HomeController implements Routable {
@FXML @FXML
private Label welcomeLabel; private Label welcomeLabel;
/**
* Called when navigating to the home view.
* Updates the welcome label with the username.
*
* @param props Navigation properties.
*/
@Override @Override
public void onNavigate(Map<String, Object> props) { public void onNavigate(Map<String, Object> props) {
String username = appState.getUsername(); String username = appState.getUsername();
@@ -27,6 +36,9 @@ public class HomeController implements Routable {
welcomeLabel.setText("Welcome, " + username + "!"); welcomeLabel.setText("Welcome, " + username + "!");
} }
/**
* Handles the logout action.
*/
@FXML @FXML
public void onLogoutClick() { public void onLogoutClick() {
appState.clear(); appState.clear();
@@ -11,6 +11,9 @@ import javafx.scene.control.*;
import java.util.Map; import java.util.Map;
import javafx.application.Platform; import javafx.application.Platform;
/**
* Controller for handling the login view.
*/
@Route(path = "/login", fxml = "/login.fxml") @Route(path = "/login", fxml = "/login.fxml")
public class LoginController implements Routable { public class LoginController implements Routable {
@@ -29,10 +32,18 @@ public class LoginController implements Routable {
@FXML @FXML
private ProgressIndicator loadingIndicator; private ProgressIndicator loadingIndicator;
/**
* Called when navigating to the login view.
*
* @param props Navigation properties.
*/
@Override @Override
public void onNavigate(Map<String, Object> props) { public void onNavigate(Map<String, Object> props) {
} }
/**
* Handles the login action. Initiates the login process in a background thread.
*/
@FXML @FXML
public void onLoginClick() { public void onLoginClick() {
final String username = usernameField.getText(); final String username = usernameField.getText();
@@ -63,6 +74,9 @@ public class LoginController implements Routable {
}, "login-thread").start(); }, "login-thread").start();
} }
/**
* Handles username action (e.g., pressing enter) by requesting focus on the password field.
*/
@FXML @FXML
public void onUsernameAction() { public void onUsernameAction() {
passwordField.requestFocus(); passwordField.requestFocus();
@@ -13,6 +13,10 @@ import javafx.scene.web.WebView;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
/**
* Controller for handling the Moodle integration view.
* Displays a WebView with the Moodle website and handles automatic login.
*/
@Route(path = "/moodle", fxml = "/moodle.fxml") @Route(path = "/moodle", fxml = "/moodle.fxml")
public class MoodleController extends DashboardBaseController { public class MoodleController extends DashboardBaseController {
@@ -33,6 +37,12 @@ public class MoodleController extends DashboardBaseController {
private WebEngine webEngine; private WebEngine webEngine;
/**
* Initializes the controller when navigating to this view.
* Sets up the WebView, auto-login logic, and navigation buttons.
*
* @param props Navigation properties, not explicitly used here.
*/
@Override @Override
public void onNavigate(Map<String, Object> props) { public void onNavigate(Map<String, Object> props) {
super.onNavigate(props); super.onNavigate(props);
@@ -104,6 +114,9 @@ public class MoodleController extends DashboardBaseController {
webEngine.load("https://moodle.spsejecna.cz/"); webEngine.load("https://moodle.spsejecna.cz/");
} }
/**
* Updates the back and forward navigation buttons state.
*/
private void updateNavigationButtons() { private void updateNavigationButtons() {
WebHistory history = webEngine.getHistory(); WebHistory history = webEngine.getHistory();
int currentIndex = history.getCurrentIndex(); int currentIndex = history.getCurrentIndex();
@@ -129,6 +142,9 @@ public class MoodleController extends DashboardBaseController {
btnForward.setDisable(validForwardIndex == -1); btnForward.setDisable(validForwardIndex == -1);
} }
/**
* Navigates back in the WebView history.
*/
@FXML @FXML
protected void onBrowserBack() { protected void onBrowserBack() {
WebHistory history = webEngine.getHistory(); WebHistory history = webEngine.getHistory();
@@ -142,6 +158,9 @@ public class MoodleController extends DashboardBaseController {
} }
} }
/**
* Navigates forward in the WebView history.
*/
@FXML @FXML
protected void onBrowserForward() { protected void onBrowserForward() {
WebHistory history = webEngine.getHistory(); WebHistory history = webEngine.getHistory();
@@ -155,11 +174,17 @@ public class MoodleController extends DashboardBaseController {
} }
} }
/**
* Reloads the current page in the WebView.
*/
@FXML @FXML
protected void onBrowserReload() { protected void onBrowserReload() {
webEngine.reload(); webEngine.reload();
} }
/**
* Handles navigation back to the dashboard.
*/
@FXML @FXML
protected void onBackToDashboard() { protected void onBackToDashboard() {
Router.getInstance().navigate("/dashboard"); Router.getInstance().navigate("/dashboard");
@@ -15,6 +15,10 @@ import javafx.scene.layout.*;
import java.util.Map; import java.util.Map;
/**
* Controller for handling the teacher profile view.
* Displays details about a specific teacher, including contact information and their timetable.
*/
@Route(path = "/teacher_profile", fxml = "/teacher_profile.fxml") @Route(path = "/teacher_profile", fxml = "/teacher_profile.fxml")
public class TeacherProfileController extends DashboardBaseController { public class TeacherProfileController extends DashboardBaseController {
@@ -44,6 +48,12 @@ public class TeacherProfileController extends DashboardBaseController {
private String teacherTag; private String teacherTag;
/**
* Initializes the controller when navigating to this view.
* Sets the teacher tag from navigation properties and initiates data loading.
*
* @param props Navigation properties, must contain a "tag" key with the teacher's tag.
*/
@Override @Override
public void onNavigate(Map<String, Object> props) { public void onNavigate(Map<String, Object> props) {
super.onNavigate(props); super.onNavigate(props);
@@ -57,6 +67,10 @@ public class TeacherProfileController extends DashboardBaseController {
loadTeacher(); loadTeacher();
} }
/**
* Fetches the teacher profile from the API and updates the UI.
* Shows a loading indicator while fetching.
*/
private void loadTeacher() { private void loadTeacher() {
loadingIndicator.setVisible(true); loadingIndicator.setVisible(true);
scrollPane.setVisible(false); scrollPane.setVisible(false);
@@ -85,6 +99,11 @@ public class TeacherProfileController extends DashboardBaseController {
}); });
} }
/**
* Renders the teacher profile information into the UI.
*
* @param teacher The teacher object to display.
*/
private void renderTeacher(Teacher teacher) { private void renderTeacher(Teacher teacher) {
headerNameLabel.setText(teacher.getFullName()); headerNameLabel.setText(teacher.getFullName());
fullNameLabel.setText(teacher.getFullName()); fullNameLabel.setText(teacher.getFullName());
@@ -127,6 +146,14 @@ public class TeacherProfileController extends DashboardBaseController {
} }
} }
/**
* Adds a row of detail information to the details grid.
*
* @param labelText The label text for the detail.
* @param value The value of the detail.
* @param row The row index in the grid.
* @return The next row index.
*/
private int addDetailRow(String labelText, String value, int row) { private int addDetailRow(String labelText, String value, int row) {
if (value == null || value.isBlank()) { if (value == null || value.isBlank()) {
return row; return row;
@@ -145,6 +172,9 @@ public class TeacherProfileController extends DashboardBaseController {
return row + 1; return row + 1;
} }
/**
* Handles navigation back to the teachers list view.
*/
@FXML @FXML
@Override @Override
protected void onBack() { protected void onBack() {
@@ -15,6 +15,10 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* Controller for handling the teachers list view.
* Provides functionality to display, search, and navigate to teacher profiles.
*/
@Route(path = "/teachers", fxml = "/teachers.fxml") @Route(path = "/teachers", fxml = "/teachers.fxml")
public class TeachersController extends DashboardBaseController { public class TeachersController extends DashboardBaseController {
@@ -32,6 +36,12 @@ public class TeachersController extends DashboardBaseController {
private List<TeacherReference> allTeachers = new ArrayList<>(); private List<TeacherReference> allTeachers = new ArrayList<>();
/**
* Initializes the controller when navigating to this view.
* Sets up the search filter and initiates data loading.
*
* @param props Navigation properties, not explicitly used here.
*/
@Override @Override
public void onNavigate(Map<String, Object> props) { public void onNavigate(Map<String, Object> props) {
super.onNavigate(props); super.onNavigate(props);
@@ -43,6 +53,10 @@ public class TeachersController extends DashboardBaseController {
loadTeachers(); loadTeachers();
} }
/**
* Fetches the list of teachers from the API and updates the UI.
* Shows a loading indicator while fetching.
*/
private void loadTeachers() { private void loadTeachers() {
loadingIndicator.setVisible(true); loadingIndicator.setVisible(true);
scrollPane.setVisible(false); scrollPane.setVisible(false);
@@ -76,6 +90,11 @@ public class TeachersController extends DashboardBaseController {
}); });
} }
/**
* Filters the list of teachers based on the provided search query.
*
* @param query The search string to filter by.
*/
private void filterTeachers(String query) { private void filterTeachers(String query) {
teachersFlow.getChildren().clear(); teachersFlow.getChildren().clear();
@@ -113,6 +132,9 @@ public class TeachersController extends DashboardBaseController {
} }
} }
/**
* Handles navigation back to the dashboard.
*/
@FXML @FXML
protected void onBackToDashboard() { protected void onBackToDashboard() {
Router.getInstance().navigate("/dashboard"); Router.getInstance().navigate("/dashboard");
@@ -21,6 +21,10 @@ import kotlinx.datetime.DayOfWeek;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* Controller for handling the timetable view.
* Displays the weekly timetable for the student.
*/
@Route(path = "/timetable", fxml = "/timetable.fxml") @Route(path = "/timetable", fxml = "/timetable.fxml")
public class TimetableController extends DashboardBaseController { public class TimetableController extends DashboardBaseController {
@@ -35,12 +39,22 @@ public class TimetableController extends DashboardBaseController {
private TimetablePage currentPage; private TimetablePage currentPage;
/**
* Initializes the controller when navigating to this view.
* Initiates loading of timetable data.
*
* @param props Navigation properties, not explicitly used here.
*/
@Override @Override
public void onNavigate(Map<String, Object> props) { public void onNavigate(Map<String, Object> props) {
super.onNavigate(props); super.onNavigate(props);
loadTimetable(); loadTimetable();
} }
/**
* Fetches the timetable page from the API and updates the UI.
* Shows a loading indicator while fetching.
*/
private void loadTimetable() { private void loadTimetable() {
loadingIndicator.setVisible(true); loadingIndicator.setVisible(true);
scrollPane.setVisible(false); scrollPane.setVisible(false);
@@ -56,6 +70,11 @@ public class TimetableController extends DashboardBaseController {
}, QueryOptions.defaultOptions()).thenAccept(this::handleTimetableResult); }, QueryOptions.defaultOptions()).thenAccept(this::handleTimetableResult);
} }
/**
* Handles the result of the timetable fetch operation.
*
* @param result The result of the fetch.
*/
private void handleTimetableResult(QueryResult<TimetablePage> result) { private void handleTimetableResult(QueryResult<TimetablePage> result) {
Platform.runLater(() -> { Platform.runLater(() -> {
loadingIndicator.setVisible(false); loadingIndicator.setVisible(false);
@@ -75,6 +94,9 @@ public class TimetableController extends DashboardBaseController {
}); });
} }
/**
* Renders the timetable grid using the TimetableRenderer.
*/
private void renderTimetable() { private void renderTimetable() {
timetableGrid.getChildren().clear(); timetableGrid.getChildren().clear();
timetableGrid.getColumnConstraints().clear(); timetableGrid.getColumnConstraints().clear();
@@ -85,6 +107,9 @@ public class TimetableController extends DashboardBaseController {
TimetableRenderer.renderTimetable(timetableGrid, timetable); TimetableRenderer.renderTimetable(timetableGrid, timetable);
} }
/**
* Handles navigation back to the dashboard.
*/
@FXML @FXML
protected void onBackToDashboard() { protected void onBackToDashboard() {
Router.getInstance().navigate("/dashboard"); Router.getInstance().navigate("/dashboard");
@@ -4,11 +4,23 @@ import java.util.Map;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Supplier; import java.util.function.Supplier;
/**
* A client for handling data fetching with caching and background updates.
*/
public class QueryClient { public class QueryClient {
private final Map<String, CachedItem<?>> cache = new ConcurrentHashMap<>(); private final Map<String, CachedItem<?>> cache = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
/**
* Fetches data with caching support.
*
* @param key The cache key.
* @param fetcher Supplier for fetching data if not cached or stale.
* @param opts Query options for caching behavior.
* @param <T> The type of data.
* @return A CompletableFuture with the result.
*/
public <T> CompletableFuture<QueryResult<T>> fetch(String key, Supplier<T> fetcher, QueryOptions opts) { public <T> CompletableFuture<QueryResult<T>> fetch(String key, Supplier<T> fetcher, QueryOptions opts) {
@SuppressWarnings("unchecked") CachedItem<T> item = (CachedItem<T>) cache.get(key); @SuppressWarnings("unchecked") CachedItem<T> item = (CachedItem<T>) cache.get(key);
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
@@ -53,14 +65,29 @@ public class QueryClient {
return future; return future;
} }
/**
* Manually sets an item in the cache.
*
* @param key The cache key.
* @param value The value to cache.
* @param <T> The type of data.
*/
public <T> void set(String key, T value) { public <T> void set(String key, T value) {
cache.put(key, new CachedItem<>(value, System.currentTimeMillis())); cache.put(key, new CachedItem<>(value, System.currentTimeMillis()));
} }
/**
* Invalidates a cache item.
*
* @param key The cache key to remove.
*/
public void invalidate(String key) { public void invalidate(String key) {
cache.remove(key); cache.remove(key);
} }
/**
* Shuts down the scheduler.
*/
public void shutdown() { public void shutdown() {
scheduler.shutdownNow(); scheduler.shutdownNow();
} }
@@ -1,17 +1,28 @@
package cz.jzitnik.query; package cz.jzitnik.query;
/**
* Configuration options for query operations, such as caching and retries.
*/
public class QueryOptions { public class QueryOptions {
// time until data is considered stale (ms) /** Time until data is considered stale (ms). Default: 5 minutes. */
public long staleTime = 5 * 60 * 1000; // 5 minutes public long staleTime = 5 * 60 * 1000;
// time until unused cache entry is evicted (ms) /** Time until unused cache entry is evicted (ms). Default: 30 minutes. */
public long cacheTime = 30 * 60 * 1000; // 30 minutes public long cacheTime = 30 * 60 * 1000;
// if >0, interval to refetch in background (ms) /** If > 0, interval to refetch in background (ms). Default: 0 (disabled). */
public long refetchInterval = 0; public long refetchInterval = 0;
// number of retry attempts on failure /** Number of retry attempts on failure. Default: 0. */
public int retryAttempts = 0; public int retryAttempts = 0;
/**
* Creates new default query options.
*/
public QueryOptions() {} public QueryOptions() {}
/**
* Returns default query options.
*
* @return Default QueryOptions instance.
*/
public static QueryOptions defaultOptions() { public static QueryOptions defaultOptions() {
return new QueryOptions(); return new QueryOptions();
} }
@@ -3,19 +3,53 @@ package cz.jzitnik.query;
import java.time.Instant; import java.time.Instant;
import java.util.Optional; import java.util.Optional;
/**
* Represents the result of a query operation.
*
* @param <T> The type of the data.
*/
public class QueryResult<T> { public class QueryResult<T> {
private final T data; private final T data;
private final Throwable error; private final Throwable error;
private final Instant fetchedAt; private final Instant fetchedAt;
/**
* Creates a new QueryResult.
*
* @param data The query data, or null if an error occurred.
* @param error The error, or null if successful.
*/
public QueryResult(T data, Throwable error) { public QueryResult(T data, Throwable error) {
this.data = data; this.data = data;
this.error = error; this.error = error;
this.fetchedAt = Instant.now(); this.fetchedAt = Instant.now();
} }
/**
* Gets the data.
*
* @return Optional containing the data.
*/
public Optional<T> getData() { return Optional.ofNullable(data); } public Optional<T> getData() { return Optional.ofNullable(data); }
/**
* Gets the error.
*
* @return Optional containing the error.
*/
public Optional<Throwable> getError() { return Optional.ofNullable(error); } public Optional<Throwable> getError() { return Optional.ofNullable(error); }
/**
* Gets the fetch time.
*
* @return The fetch time.
*/
public Instant getFetchedAt() { return fetchedAt; } public Instant getFetchedAt() { return fetchedAt; }
/**
* Checks if the query was successful.
*
* @return True if successful, false otherwise.
*/
public boolean isSuccess() { return error == null; } public boolean isSuccess() { return error == null; }
} }
@@ -11,6 +11,9 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
/**
* A simple router for navigating between views in a JavaFX application.
*/
public class Router { public class Router {
private static Router instance; private static Router instance;
private Pane rootContainer; private Pane rootContainer;
@@ -18,6 +21,11 @@ public class Router {
private Router() {} private Router() {}
/**
* Gets the singleton instance of the router.
*
* @return The Router instance.
*/
public static Router getInstance() { public static Router getInstance() {
if (instance == null) { if (instance == null) {
instance = new Router(); instance = new Router();
@@ -25,10 +33,20 @@ public class Router {
return instance; return instance;
} }
/**
* Sets the root container pane for the router.
*
* @param rootContainer The root container.
*/
public void setRootContainer(Pane rootContainer) { public void setRootContainer(Pane rootContainer) {
this.rootContainer = rootContainer; this.rootContainer = rootContainer;
} }
/**
* Registers routes annotated with @Route in the specified base package.
*
* @param basePackage The base package to scan for routes.
*/
public void registerAnnotatedRoutes(String basePackage) { public void registerAnnotatedRoutes(String basePackage) {
Reflections r = new Reflections(basePackage); Reflections r = new Reflections(basePackage);
Set<Class<?>> controllers = r.getTypesAnnotatedWith(Route.class); Set<Class<?>> controllers = r.getTypesAnnotatedWith(Route.class);
@@ -40,10 +58,21 @@ public class Router {
} }
} }
/**
* Navigates to a specific path.
*
* @param path The path to navigate to.
*/
public void navigate(String path) { public void navigate(String path) {
navigate(path, new HashMap<>()); navigate(path, new HashMap<>());
} }
/**
* Navigates to a specific path with navigation properties.
*
* @param path The path to navigate to.
* @param props Navigation properties.
*/
public void navigate(String path, Map<String, Object> props) { public void navigate(String path, Map<String, Object> props) {
if (rootContainer == null) { if (rootContainer == null) {
throw new IllegalStateException("Root container not set in Router."); throw new IllegalStateException("Root container not set in Router.");
@@ -4,6 +4,11 @@ import cz.jzitnik.auth.CredentialStore;
import io.github.tomhula.jecnaapi.java.JecnaClientJavaWrapper; import io.github.tomhula.jecnaapi.java.JecnaClientJavaWrapper;
import cz.jzitnik.query.QueryClient; import cz.jzitnik.query.QueryClient;
/**
* Manages the current application state, including user authentication and API interactions.
* This class serves as the central hub for tracking the logged-in user, managing
* the active Jecna API client session, and providing access to query tools.
*/
@State @State
public class AppState { public class AppState {
@@ -12,26 +17,59 @@ public class AppState {
private JecnaClientJavaWrapper client; private JecnaClientJavaWrapper client;
private QueryClient queryClient; private QueryClient queryClient;
/**
* Gets the username of the currently logged-in user.
*
* @return the username, or null if no user is logged in.
*/
public String getUsername() { public String getUsername() {
return username; return username;
} }
/**
* Gets the password of the currently logged-in user.
*
* @return the password, or null if no user is logged in.
*/
public String getPassword() { public String getPassword() {
return password; return password;
} }
/**
* Gets the active Jecna API client wrapper.
*
* @return the Jecna API client instance, or null if not initialized/logged in.
*/
public JecnaClientJavaWrapper getClient() { public JecnaClientJavaWrapper getClient() {
return client; return client;
} }
/**
* Gets the query client used for fetching specific data.
*
* @return the QueryClient instance.
*/
public QueryClient getQueryClient() { public QueryClient getQueryClient() {
return queryClient; return queryClient;
} }
/**
* Sets the query client for this state.
*
* @param qc the QueryClient instance to use.
*/
public void setQueryClient(QueryClient qc) { public void setQueryClient(QueryClient qc) {
this.queryClient = qc; this.queryClient = qc;
} }
/**
* Attempts to log the user into the Jecna API with the provided credentials.
* If successful, it stores the credentials securely for future sessions.
*
* @param username the user's username.
* @param password the user's password.
* @return true if login was successful, false otherwise.
*/
public boolean login(String username, String password) { public boolean login(String username, String password) {
if (username == null || username.isEmpty() || password == null) return false; if (username == null || username.isEmpty() || password == null) return false;
@@ -61,6 +99,10 @@ public class AppState {
} }
} }
/**
* Clears the current application state, including logging out the user
* and resetting the API client.
*/
public void clear() { public void clear() {
this.username = null; this.username = null;
this.password = null; this.password = null;
@@ -11,8 +11,14 @@ import javafx.scene.paint.Color;
import javafx.scene.shape.Circle; import javafx.scene.shape.Circle;
import javafx.util.Duration; import javafx.util.Duration;
/**
* A UI component that displays an animated aurora-like background.
*/
public class AuroraBackground extends StackPane { public class AuroraBackground extends StackPane {
/**
* Initializes the aurora background with animated blobs of color.
*/
public AuroraBackground() { public AuroraBackground() {
Color color1 = Color.web("#00FFFF", 0.10); // Cyan Color color1 = Color.web("#00FFFF", 0.10); // Cyan
Color color2 = Color.web("#FF00FF", 0.10); // Magenta Color color2 = Color.web("#FF00FF", 0.10); // Magenta
@@ -38,6 +44,16 @@ public class AuroraBackground extends StackPane {
setupAnimation(blob3, 0, -200, 0, 200, 15); setupAnimation(blob3, 0, -200, 0, 200, 15);
} }
/**
* Sets up the animation for a color blob.
*
* @param node The circle node to animate.
* @param startX Starting X translation.
* @param startY Starting Y translation.
* @param endX Ending X translation.
* @param endY Ending Y translation.
* @param durationSeconds Duration of the animation in seconds.
*/
private void setupAnimation(Circle node, double startX, double startY, double endX, double endY, int durationSeconds) { private void setupAnimation(Circle node, double startX, double startY, double endX, double endY, int durationSeconds) {
Timeline timeline = new Timeline( Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO, new KeyFrame(Duration.ZERO,
@@ -8,8 +8,20 @@ import java.net.URL;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream; import java.util.zip.InflaterInputStream;
/**
* A little utility to grab images from the school website.
* Since the server can be picky about requests, we have to pretend
* to be a real browser to get the files successfully.
*/
public class ImageFetcher { public class ImageFetcher {
/**
* Fetches an image from the school server, handling decompression
* and proper headers to avoid being blocked.
*
* @param imagePath The relative path to the image on the school server.
* @return The loaded JavaFX Image, or null if something went wrong.
*/
public static Image fetchImage(String imagePath) { public static Image fetchImage(String imagePath) {
if (imagePath == null || imagePath.isEmpty()) { if (imagePath == null || imagePath.isEmpty()) {
return null; return null;
@@ -7,8 +7,18 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.input.ScrollEvent; import javafx.scene.input.ScrollEvent;
import javafx.util.Duration; import javafx.util.Duration;
/**
* Helps make scrolling through the app feel a bit more premium.
* Instead of jumping straight to the new position, this gently animates
* the scroll, making it much easier on the eyes.
*/
public class ScrollUtils { public class ScrollUtils {
/**
* Enables smooth, animated scrolling for a given ScrollPane.
*
* @param scrollPane The ScrollPane we want to make feel smoother.
*/
public static void enableSmoothScrolling(ScrollPane scrollPane) { public static void enableSmoothScrolling(ScrollPane scrollPane) {
scrollPane.addEventFilter(ScrollEvent.SCROLL, event -> { scrollPane.addEventFilter(ScrollEvent.SCROLL, event -> {
double deltaY = event.getDeltaY(); double deltaY = event.getDeltaY();
@@ -21,8 +21,17 @@ import kotlinx.datetime.DayOfWeek;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* Utility class for rendering the timetable in a JavaFX GridPane.
*/
public class TimetableRenderer { public class TimetableRenderer {
/**
* Renders the timetable into the provided GridPane.
*
* @param timetableGrid The GridPane to render into.
* @param timetable The timetable data to render.
*/
public static void renderTimetable(GridPane timetableGrid, Timetable timetable) { public static void renderTimetable(GridPane timetableGrid, Timetable timetable) {
timetableGrid.getChildren().clear(); timetableGrid.getChildren().clear();
timetableGrid.getColumnConstraints().clear(); timetableGrid.getColumnConstraints().clear();
@@ -92,6 +101,12 @@ public class TimetableRenderer {
} }
} }
/**
* Creates a cell UI component for a lesson spot.
*
* @param spot The lesson spot data.
* @return The created VBox component.
*/
private static VBox createLessonCell(LessonSpot spot) { private static VBox createLessonCell(LessonSpot spot) {
VBox cell = new VBox(5); VBox cell = new VBox(5);
cell.setAlignment(Pos.CENTER); cell.setAlignment(Pos.CENTER);
@@ -131,6 +146,12 @@ public class TimetableRenderer {
return cell; return cell;
} }
/**
* Adds lesson information to a cell UI component.
*
* @param cell The cell UI component.
* @param lesson The lesson data.
*/
private static void addLessonInfoToCell(VBox cell, Lesson lesson) { private static void addLessonInfoToCell(VBox cell, Lesson lesson) {
Label subjectLbl = new Label(lesson.getSubjectName().getShort() != null ? lesson.getSubjectName().getShort() : lesson.getSubjectName().getFull()); Label subjectLbl = new Label(lesson.getSubjectName().getShort() != null ? lesson.getSubjectName().getShort() : lesson.getSubjectName().getFull());
subjectLbl.getStyleClass().add("timetable-subject"); subjectLbl.getStyleClass().add("timetable-subject");
@@ -153,6 +174,11 @@ public class TimetableRenderer {
cell.getChildren().addAll(subjectLbl, bottomInfo); cell.getChildren().addAll(subjectLbl, bottomInfo);
} }
/**
* Shows a dialog with detailed lesson information.
*
* @param spot The lesson spot data.
*/
private static void showLessonDetails(LessonSpot spot) { private static void showLessonDetails(LessonSpot spot) {
Dialog<Void> dialog = new Dialog<>(); Dialog<Void> dialog = new Dialog<>();
dialog.setTitle("Detail hodiny"); dialog.setTitle("Detail hodiny");
@@ -209,6 +235,12 @@ public class TimetableRenderer {
dialog.show(); dialog.show();
} }
/**
* Translates DayOfWeek to a Czech string.
*
* @param day The DayOfWeek.
* @return The translated day name.
*/
private static String translateDay(DayOfWeek day) { private static String translateDay(DayOfWeek day) {
return switch (day) { return switch (day) {
case MONDAY -> "Pondělí"; case MONDAY -> "Pondělí";